Specifically with Material UI, I'm trying to render a large number of inputs (> 100) and manage a global state effectively. I run into serious performance issues like input lag at around 50 inputs but the lag is still noticeable at around 10 inputs. I've narrowed it down to the Material UI <TextField /> component because if I change that to regular input element, there is virtually no lag which is to be expected since there's a lot more happening in a MUI component.
What I don't know however, is if changing how I manage state might help performance. I've tried 2 state management approaches: traditional react with useState hook and Recoil.
In addition to the 2 approaches above, I also tried a combination of recoil and traditional react by using useState and useSetRecoilState in filter components so that all filters wouldn't re-render on change to recoil state since they are only writing not reading but still no luck.
EDIT
Turns out I forgot to test putting values in more than 1 input... When you enter a value in another input, the previously entered input will get wiped. I'm thinking it's because in my handleChange function, I replace an item in filterList but filterList is now an old outdated reference so it does not have the previously set value. Back at square one now, because if I read state in the filter component, then the component will re-render which eliminates the memo performance boost.
EDIT 2
Posted solution below
Looks like I was trying to use recoil when I really didn't need to. Using React.memo along with React.useState and moving the global state edit up to the parent ended up being all I needed.
Filters.js
const filterMaker = (length) => {
let filters = [];
for (let i = 0; i < length; i++) {
if (i % 2 === 0) {
filters.push({ name: `filter-${i}`, type: 'a', active: { value1: '' } });
} else {
filters.push({
name: `filter-${i}`,
type: 'b',
active: { value1: '', value2: '' },
});
}
}
return filters;
};
function replaceItemAtIndex(arr, index, newValue) {
return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
}
export default function Filters() {
const [filterList, setFilterList] = useRecoilState(filterListState);
useEffect(() => {
setFilterList(filterMaker(200));
}, []);
const handleChange = (filter, name, value, index) => {
setFilterList((prevState) => {
return replaceItemAtIndex(prevState, index, {
...filter,
active: {
...filter.active,
[name]: value,
},
});
});
};
return (
<div>
{filterList.map((filter) =>
filter.type === 'a' ? (
<FilterA
key={filter.name}
filter={filter}
filterList={filterList}
handleChange={handleChange}
/>
) : (
<FilterB
key={filter.name}
filter={filter}
filterList={filterList}
handleChange={handleChange}
/>
)
)}
</div>
);
}
FilterA.js
export const FilterA = memo(
({ filter, filterList, handleChange }) => {
const [values, setValues] = useState(filter.active || {});
const index = filterList.findIndex((item) => item.name === filter.name);
const handleLocalChange = ({ target: { name, value } }) => {
setValues((prevState) => {
return { ...prevState, [name]: value };
});
handleChange(filter, name, value, index);
};
return (
<Box mb={1}>
<Typography>{filter.name}</Typography>
<TextField
variant="standard"
name="value1"
label="value1"
value={values.value1 || ''}
onChange={handleLocalChange}
/>
</Box>
);
},
(prevProps, nextProps) => {
if (prevProps.filter.active.value1 === nextProps.filter.active.value1) {
return true;
}
return false;
}
);
Related
I would like to use the awesome react-widgets DropDownList to load records on demand from the server.
My data load all seems to be working. But when the data prop changes, the DropDownList component is not displaying items, I get a message
The filter returned no results
Even though I see the data is populated in my component in the useEffect hook logging the data.length below.
I think this may be due to the "filter" prop doing some kind of client side filtering, but enabling this is how I get an input control to enter the search term and it does fire "onSearch"
Also, if I use my own component for display with props valueComponent or listComponent it bombs I believe when the list is initially empty.
What am I doing wrong? Can I use react-widgets DropDownList to load data on demand in this manner?
//const ItemComponent = ({item}) => <span>{item.id}: {item.name}</span>;
const DropDownUi = ({data, searching, fetchData}) => {
const onSearch = (search) => {
fetchData(search);
}
// I can see the data coming back here!
useEffect(() => {
console.log(data.length);
}, [data]);
<DropDownList
data={data}
filter
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
Got it! This issue is with the filter prop that you are passing to the component. The filter cannot take a true as value otherwise that would lead to abrupt behavior like the one you are experiencing.
This usage shall fix your problem:
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix 😅
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
Working demo
The entire code that I had tried at codesandbox:
Warning: You might have to handle the clearing of the values when the input is empty.
I thought that the logic for this was irrelevant to the problem statement. If you want, I can update that as well.
Also, I added a fakeAPI when searchTerm changes that resolves a mocked data in 2 seconds(fake timeout to see loading state).
import * as React from "react";
import "./styles.css";
import { DropdownList } from "react-widgets";
import "react-widgets/dist/css/react-widgets.css";
// Coutesy: https://usehooks.com/useDebounce
import useDebounce from "./useDebounce";
interface IData {
id: string;
name: string;
}
const fakeAPI = () =>
new Promise<IData[]>((resolve) => {
window.setTimeout(() => {
resolve([
{
name: "NA",
id: "user210757"
},
{
name: "Yash",
id: "id-1"
}
]);
}, 2000);
});
export default function App() {
const [state, ss] = React.useState<{
searching: boolean;
data: IData[];
searchTerm: string;
}>({
data: [],
searching: false,
searchTerm: ""
});
const debounceSearchTerm = useDebounce(state.searchTerm, 1200);
const setState = (obj: Record<string, any>) =>
ss((prevState) => ({ ...prevState, ...obj }));
const getData = () => {
console.log("getting data...");
setState({ searching: true });
fakeAPI().then((response) => {
console.log("response: ", response);
setState({ searching: false, data: response });
});
};
React.useEffect(() => {
if (debounceSearchTerm) {
getData();
}
}, [debounceSearchTerm]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<DropdownList
data={state.data}
filter={() => true} // This was the miss/fix 😅
valueField={"id"}
textField={"name"}
busy={state.searching}
searchTerm={state.searchTerm}
onSearch={(searchTerm) => setState({ searchTerm })}
busySpinner={<span className="fas fa-sync fa-spin" />}
delay={2000}
/>
</div>
);
}
Let me know if you have more queries on this 😇
So it i think that list should be loaded a then you can filtering your loaded data.In your example on the beginning you don't have value so list is empty, you tape in some text and then value of list re render but it look like is not filtered.....
However I look through code base, and it's look like is not ready until you don't set manually open prop drop down list component. In getDerivedStateFromprops, next data list is read only if in next props is open set. to true
From DropDwonList
static getDerivedStateFromProps(nextProps, prevState) {
let {
open,
value,
data,
messages,
searchTerm,
filter,
minLength,
caseSensitive,
} = nextProps
const { focusedItem } = prevState
const accessors = getAccessors(nextProps)
const valueChanged = value !== prevState.lastValue
let initialIdx = valueChanged && accessors.indexOf(data, value)
//-->> --- -- --- -- -- -- -- - - - - - - - - - --- - - --------
//-->>
if (open)
data = Filter.filter(data, {
filter,
searchTerm,
minLength,
caseSensitive,
textField: accessors.text,
})
const list = reduceToListState(data, prevState.list, { nextProps })
const selectedItem = data[initialIdx]
const nextFocusedItem = ~data.indexOf(focusedItem) ? focusedItem : data[0]
return {
data,
list,
accessors,
lastValue: value,
messages: getMessages(messages),
selectedItem: valueChanged
? list.nextEnabled(selectedItem)
: prevState.selectedItem,
focusedItem:
(valueChanged || focusedItem === undefined)
? list.nextEnabled(selectedItem !== undefined ? selectedItem : nextFocusedItem)
: nextFocusedItem,
}
}
I would try:
<DropDownList
data={data}
filter
open
valueField={id}
textField={name}
onSearch={onSearch}
busy={searching} />
};
if it will be works, then you just have to
manage your open state by yourself.
I am building a nested form framework that uses the redux form and material ui framework -- I've built the components here to date - https://codesandbox.io/s/heuristic-hopper-lzekw
what I would like to do - is append some "animation" to the fields - to mimick typing -- I've achieved this by a little function that will take the initial text and step through the chars -- updating the prop of the initial values for that field.
Now the problem I have -- is I need to create an onClick on the textField -- and if its an automated typetext field - reset the value to an empty string -- pass this onclick back up to the parent shells -- and even back up to the typetext function to break the timeout --- so if the user loads the page they see the text typing - but with UI functionality improvements - if I click on the field during mid-animation - I want the animation to stop/break, I want the field to clear.
I want to have control over also which fields should be cleared -- so in this case - have a param - that indicates onClickClear: true -- so as not to break user edit profile pre-filled forms.
===sandbox with no typetext -- but a good base for how to glue these two frameworks together
https://codesandbox.io/s/heuristic-hopper-lzekw?file=/src/Home.js
== this is the latest sandbox with auto typing as it currently is
https://codesandbox.io/s/amazing-bell-z8nhf
var self = this;
typeAnimation(this.state.initial_search_term.search_term, 100, function(msg){
self.setState({
initial_search_term: {"search_term": msg}
});
});
I think that update the placeholder property using the input ref is a good solution, that way you don't need to update the input value (avoid component re renders), and you can clear the placeholder text on click event:
Home.js
class Home extends Component {
constructor(props, context) {
super(props, context);
this.searchInputRef = React.createRef(null);
this.state = { initial_search_term: { search_term: "" } };
}
componentDidMount() {
var self = this;
typeAnimation("Start typing...", 100, function (msg) {
if (document.activeElement !== self.searchInputRef.current) {
self.searchInputRef.current.setAttribute("placeholder", msg);
} else {
return true; // stop typings
}
});
}
render() {
//...
let fieldsSearchForm = [
{
id: "search-field",
type: "text",
label: "Search Term",
name: ["search_term"],
options: [],
fieldRef: this.searchInputRef,
onClick: () => (this.searchInputRef.current.placeholder = "")
}
];
//...
}
}
FieldMaker.js
class FieldMaker extends Component {
//...
render() {
return (
<>
{this.state.fields.map((item, j) => {
if (item.visibility) {
if (item.type !== "checkbox") {
return (
<Field
id={item.id}
//...other props
fieldRef={item.fieldRef}
onClick={item.onClick}
/>
);
} else {
//...
}
} else {
//...
}
})}
</>
);
}
}
renderTextField.js
const renderTextField = ({
id,
input,
rows,
multiline,
label,
type,
meta: { touched, error, warning },
onClick,
fieldRef
}) => (
<FormControl
component="fieldset"
fullWidth={true}
className={multiline === true ? "has-multiline" : null}
>
<TextField
id={id}
inputRef={fieldRef}
onClick={onClick}
// other props
/>
</FormControl>
);
Utility.js
export async function typeAnimation(text, timing, callback) {
let concatStr = "";
for (const char of text) {
concatStr += char;
await sleep(timing);
const shouldStop = callback(concatStr);
if (shouldStop) break; // stop the loop
}
}
styles.css
// to keep the placeholder visible
#search-field-label {
transform: translate(0, 1.5px) scale(0.75);
transform-origin: top left;
}
#search-field::-webkit-input-placeholder {
opacity: 1 !important;
}
I know this isn't the answer you are looking for, but the easiest path would be to animate the placeholder text instead of the main input text. Then you don't have to worry about anything and can just let the animation play out regardless of the user's actions.
Now the problem I have -- is I need to create an onClick on the
textField -- and if its an automated typetext field - reset the value
to an empty string -- pass this onclick back up to the parent shells
-- and even back up to the typetext function to break the timeout --- so if the user loads the page they see the text typing - but with UI
functionality improvements - if I click on the field during
mid-animation - I want the animation to stop/break, I want the field
to clear.
I want to have control over also which fields should be cleared -- so
in this case - have a param - that indicates onClickClear: true -- so
as not to break user edit profile pre-filled forms.
All of this is satisfied by using the placeholder of the field instead (although the typetext won't stop because there's no need to as the user's text / prefilled text will hide the placeholders). The only thing I haven't hooked up is to stop the typetext on Home's componentWillUnmount as without that, it would throw warning messages that setState is being called on an unmounted component.
I had to do some refactoring as there were some issues with things like mutating React state (toggleFieldVisibility in FieldMaker.js) and not updating this.state.fields when new props are passed down as the state was only being set in the constructor. I also renamed some things within FieldMaker.js while I was at it (mostly due to personal preference in this case).
There are definitely problems with trying to derive state from props regardless of how you do it: You Probably Don't Need Derived State
Running code:
https://codesandbox.io/s/busy-davinci-mk0dq?file=/src/Home.js
Home.js
state = {
initial_search_term: { search_term: "" },
searchPlaceholder: "",
textPlaceholder: "",
valPlaceholder: ""
};
componentDidMount() {
typeAnimation("Search Text...", 100, (msg) => {
this.setState({
searchPlaceholder: msg
});
});
typeAnimation("Just some super long text you used to know", 100, (msg) => {
this.setState({
textPlaceholder: msg
});
});
typeAnimation("I'm a value, but am I valuable??", 100, (msg) => {
this.setState({
valPlaceholder: msg
});
});
}
// Render funct:
let fieldsSearchForm = [
{
type: "text",
label: "Search Term",
name: ["search_term"],
props: { placeholder: this.state.searchPlaceholder },
options: []
},
{
type: "text",
label: "Text",
name: ["test"],
props: { placeholder: this.state.textPlaceholder },
options: []
},
{
type: "text",
label: "Value",
name: ["test2"],
props: { placeholder: this.state.valPlaceholder }
}
];
FieldMaker.js
The getDerivedStateFromProps is the real main difference here, This is to populate the subs array based on the fields whenever the fields change (and to set visibility). I have no idea how much of that is really necessary as there's no notion of what any of that is actually supposed to do in this. So it likely needs more work to get it fully working.
The other difference is a bit of a refactor to having a separate visiblity object in state rather than modifying the fields in state.
The main reason for modifying this file is to make sure that updates to the fields prop translates to updates to the children Fields so that the placeholder can be passed down via props to the Field and thus the renderTextField
state = {
visibility: {}
};
static getDerivedStateFromProps(props, state) {
let newState = { prevFields: props.fields };
if (props.fields !== state.prevFields) {
let visibility = state.visibility;
let subs = props.fields.reduce((subs, field) => {
if (field.sub) {
subs.push(field.sub);
visibility[field.name] = false;
} else {
visibility[field.name] = true;
}
return subs;
}, []);
newState.subs = subs;
}
return newState;
}
toggleFieldVisibility(pos, isVisibile) {
let field = this.props.fields[pos].name;
this.setState((prev) => {
return { ...prev, [field]: isVisibile };
});
// This directly manipulates state, and is likely problematic in React
// let fields = { ...this.state.fields };
// fields[pos]["visibility"] = isVisibile;
}
componentDidMount() {
this.hideSubs();
}
// In render:
return (
<>
{this.props.fields.map((item, j) => {
if (this.state.visibility[item.name]) {
if (item.type !== "checkbox") {
return (
<Field
key={j}
name={item.name[0]}
props={item.props}
label={item.label}
// ...
renderTextField.js
In this, the goal of the change is just to pass the placeholder down to the MUI TextField, and to make the MUI TextField's label shrink back by setting InputLabelProps = {shrink: true}
const renderTextField = ({
input,
rows,
multiline,
label,
type,
meta: { touched, error, warning },
placeholder,
InputLabelProps
}) => {
// Ensure that the label is shrunk to the top of the input
// whenever there's a placeholder set
InputLabelProps = placeholder
? { ...(InputLabelProps ?? {}), shrink: true }
: InputLabelProps;
return (
<FormControl
component="fieldset"
fullWidth={true}
className={multiline === true ? "has-multiline" : null}
>
<TextField
InputLabelProps={InputLabelProps}
placeholder={placeholder}
label={label}
multiline={multiline}
rows={rows}
type={type}
error={touched && (error && error.length > 0 ? true : false)}
helperText={
touched &&
((error && error.length > 0 ? error : null) ||
(warning && warning.length > 0 ? warning : null))
}
{...input}
/>
</FormControl>
);
};
I redid the solution in a very quick and dirty way to avoid the pitfalls that exist in the FieldMaker file that initially caused issues in the original solution:
https://codesandbox.io/s/fervent-moser-0qtvu?file=/src/Home.js
I modified typeAnimation to support a semblance of cancellation by returning a cancel function that stops the looping and sets uses the callback to set the value to the end state.
export function typeAnimation(text, timing, callback) {
let concatStr = "";
let canceled = false;
function cancel() {
canceled = true;
}
async function runAnimation() {
for (const char of text) {
concatStr += char;
await sleep(timing);
if (canceled) {
break;
}
callback(concatStr);
}
if (canceled) {
callback(text);
}
}
runAnimation();
return cancel;
}
Then in Home.js, I modified intitial state and the componentDidMount to work with the placeholders and give me a location to store the cancel functions.
constructor(props, context) {
super(props, context);
this.state = {
initial_search_term: { search_term: "" },
placeholders: { search_term: "" }
};
}
cancelAnimations = {};
componentDidMount() {
var self = this;
this.cancelAnimations.search_term = typeAnimation(
"Start typing...",
100,
function (msg) {
self.setState((state) => ({
placeholders: { ...state.placeholders, search_term: msg }
}));
}
);
}
I also add fieldsExtras and pass that all the way down to the FieldMaker component to add extra props to the Field in that component via the index matching the fieldsSearchForm array.
let fieldsExtras = [
{
placeholder: this.state.placeholders.search_term,
onClick: this.cancelAnimations.search_term
}
];
Then once the extra props have been passed all the way down into the Field, in renderTextField, I do the same sort of thing as before, but I also added the onClick to call the passed in onClick function
const renderTextField = ({
input,
rows,
multiline,
label,
type,
meta: { touched, error, warning },
placeholder,
onClick,
InputLabelProps
}) => {
InputLabelProps = placeholder
? { ...(InputLabelProps ?? {}), shrink: true }
: InputLabelProps;
return (
<FormControl
component="fieldset"
fullWidth={true}
className={multiline === true ? "has-multiline" : null}
>
<TextField
placeholder={placeholder}
InputLabelProps={InputLabelProps}
onClick={(e, value) => {
onClick && onClick(e);
}}
label={label}
multiline={multiline}
rows={rows}
type={type}
error={touched && (error && error.length > 0 ? true : false)}
helperText={
touched &&
((error && error.length > 0 ? error : null) ||
(warning && warning.length > 0 ? warning : null))
}
{...input}
/>
</FormControl>
);
};
I have a function I calling it in render() method
and it's setState a Flag from the state.
So I got this error
Cannot update during an existing state transition (such as within
render).
I read about this error, and what I understand it's because I setState in render method and this is the wrong way.
So I'm forced to do it if u have any idea to handle this tell me.
The main idea about this function
I have an array of an object "Name as Tools" so in every object I have "id, name, count, price"
so that will render a three input in UI like this
and I have a boolean flag in-state "isEmpty" that checks every input in array before sending this data to the database.
Code
State = {
toolsUsed: [
{
id: 0,
name: '',
price: '',
count: '',
},
],
// Checker
isEmpty: false,
}
renderToolsUsed = () => {
const {toolsUsed} = this.state;
const tools = toolsUsed.map((item, i) => {
const {count, price, name, id} = item;
this.setState({
isEmpty: ['name', 'price', 'count'].every(key => item[key].length > 0),
});
return (
<View key={i} style={styles.tools}>
.... Inputs here ...
</View>
);
});
return tools;
};
JSX
render() {
return (
<View>
{this.renderToolsUsed()}
</View>
);
}
You can't update the state like this. It is like infinite loop. setState will trigger render, then render will trigger another setState, then you keep repeat the circle.
I don't know why you need isEmpty when you already have toolsUsed which you can use it to check if all input are empty.
Lets say if you insist to have isEmpty, then you can set it inside input change event.
The code is not tesed. I wrote the code directly from browser. But you can get the idea before the code.
renderToolsUsed = () => {
const { toolsUsed } = this.state;
const tools = toolsUsed.map((item, i) => {
return (
<View key={i} style={styles.tools}>
<TextInput value={item.name} onChangeText={(text) => {
this.setState({
toolsUsed: [
...toolsUsed.slice(0, i - 1),
{...item, name: text },
...toolsUSed.slice(i)
]
}, this.updateEmptyState)
}>
// other input here
</View>
);
});
// ...
};
updateEmptyState = () => {
this.setState({
isEmpty: this.state.toolsUsed.every(x => x.name === '' && x.price === '' && x.count === '')
})
}
The state is not designed to store all the data you have in the app
For what you need isEmpty inside the state?
To do this, use a global variable
Or check it out when you want it out of the render
I have a child component called First which is implemented below:
function First(props) {
const handleButtonClick = () => {
props.positiveCallback({key: 'positive', value: 'pos'})
props.negativeCallback({key: 'negative', value: '-100'})
}
return (
<div><button onClick={() => handleButtonClick()}>FIRST</button></div>
)
}
And I have App.js component.
function App() {
const [counter, setCounter] = useState({positive: '+', negative: '-'})
const handleCounterCallback = (obj) => {
console.log(obj)
let newCounter = {...counter}
newCounter[obj.key] = obj.value
setCounter(newCounter)
}
const handleDisplayClick = () => {
console.log(counter)
}
return (
<div className="App">
<First positiveCallback = {handleCounterCallback} negativeCallback = {handleCounterCallback} />
<Second negativeCallback = {handleCounterCallback} />
<button onClick={() => handleDisplayClick()}>Display</button>
</div>
);
}
When handleButtonClick is clicked in First component it triggers multiple callbacks but only the last callback updates the state.
In the example:
props.positiveCallback({key: 'positive', value: 'pos'}) // not updated
props.negativeCallback({key: 'negative', value: '-100'}) // updated
Any ideas?
Both are updating the state, your problem is the last one is overwriting the first when you spread the previous state (which isn't updated by the time your accessing it, so you are spreading the initial state). An easy workaround is to split counter into smaller pieces and update them individually
const [positive, setPositive] = useState('+')
const [negative, setNegative] = useState('-')
//This prevents your current code of breaking when accessing counter[key]
const counter = { positive, negative }
const handleCounterCallback = ({ key, value }) => {
key === 'positive' ? setPositive(value) : setNegative(value)
}
You can do that but useState setter is async like this.setState. If you want to base on the previous value you should use setter as function and you can store it in one state - change handleCounterCallback to
const handleCounterCallback = ({key,value}) => {
setCounter(prev=>({...prev, [key]: value}))
}
and that is all. Always if you want to base on the previous state use setter for the state as function.
I recommend you to use another hook rather than useState which is useReducer - I think it will be better for you
I am trying to solve a problem that happens in react app. In one of the views (components) i have a management tools that operate on big data. Basically when view loads i have componentDidMount that triggers ajax fetch that downloads array populated by around 50.000 records. Each array row is an object that has 8-10 key-value pairs.
import React, { Component } from "react";
import { List } from "react-virtualized";
import Select from "react-select";
class Market extends Component {
state = {
sports: [], // ~ 100 items
settlements: [], // ~ 50k items
selected: {
sport: null,
settlement: null
}
};
componentDidMount() {
this.getSports();
this.getSettlements();
}
getSports = async () => {
let response = await Ajax.get(API.sports);
if (response === undefined) {
return false;
}
this.setState({ sports: response.data });
};
getSettlements = async () => {
let response = await Ajax.get(API.settlements);
if (response === undefined) {
return false;
}
this.setState({ settlements: response.data });
};
save = (key, option) => {
let selected = { ...this.state.selected };
selected[key] = option;
this.setState({ selected });
};
virtualizedMenu = props => {
const rows = props.children;
const rowRenderer = ({ key, index, isScrolling, isVisible, style }) => (
<div key={key} style={style}>
{rows[index]}
</div>
);
return (
<List
style={{ width: "100%" }}
width={300}
height={300}
rowHeight={30}
rowCount={rows.length || 1}
rowRenderer={rowRenderer}
/>
);
};
render() {
const MenuList = this.virtualizedMenu;
return (
<div>
<Select
value={this.state.selected.sport}
options={this.state.sports.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("sport", option)}
/>
<Select
components={{ MenuList }}
value={this.state.selected.settlement}
options={this.state.settlements.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("settlement", option)}
/>
</div>
);
}
}
The problem i am experiencing is that after that big data is downloaded and saved to view state, even if i want to update value using select that has ~100 records it takes few seconds to do so. For example imagine that smallData is array of 100 items just { id: n, name: 'xyz' } and selectedFromSmallData is just single item from data array, selected with html select.
making a selection before big data loads takes few ms, but after data is loaded and saved to state it suddenly takes 2-4 seconds.
What would possibly help to solve that problem (unfortunately i cannot paginate that data, its not anything i have access to).
.map() creates a new array on every render. To avoid that you have three options:
store state.sports and state.settlements already prepared for Select
every time you change state.sports or state.settlements also change state.sportsOptions or state.settlementsOptions
use componentDidUpdate to update state.*Options:
The third option might be easier to implement. But it will trigger an additional rerender:
componentDidUpdate(prevProps, prevState) {
if (prevState.sports !== this.state.sports) {
this.setState(oldState => ({sportsOptions: oldState.sports.map(...)}));
}
...
}
Your onChange handlers are recreated every render and may trigger unnecessary rerendering of Select. Create two separate methods to avoid that:
saveSports = option => this.save("sport", option)
...
render() {
...
<Select onChange={this.saveSports}/>
...
}
You have similar problem with components={{ MenuList }}. Move this to the state or to the constructor so {MenuList} object is created only once. You should end up with something like this:
<Select
components={this.MenuList}
value={this.state.selected.settlement}
options={this.state.settlementsOptions}
onChange={this.saveSettlements}
/>
If this doesn't help consider using the default select and use a PureComponent to render its options. Or try to use custom PureComponents to render parts of the Select.
Also check React-select is slow when you have more than 1000 items
The size of the array shouldn't be a problem, because only the reference is stored in the state object, and react doesn't do any deep equality on state.
Maybe your render or componentDidUpdate iterates over this big array and that causes the problem.
Try to profile your app if this doesn't help.