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;
}
);