I am having an issue with a Next-js
React
checkbox list snippet after extracting it into the sandbox.
whenever I clicked the checkbox, I get the error:
TypeError: Cannot read properties of undefined (reading 'id')
which originated from line 264
:
setCheckedThread(prev => new Set(prev.add(pageData.currentThreads[index].id)));
but at the top of the index.js
I have defined the static JSON
and in useEffect()
I update the pageData
state with:
setPageData({
currentPage: threadsDataJSON.threads.current_page,
currentThreads: threadsDataJSON.threads.data,
totalPages: totalPages,
totalThreads: threadsDataJSON.threads.total,
});
so why when I clicked the checkbox
it throws the error?
my sandbox link: https://codesandbox.io/s/infallible-goldberg-vfu0ve?file=/pages/index.js
CodePudding user response:
It looks like your useEffect
on line 280 only triggers once you've checked a box (for some reason), so until you trigger that useEffect
, pageData.currentThreads
remains empty, which is where the error you're running into comes from.
I'd suggest moving all the state initialization from the useEffect
into the useState
call itself. E.g.
// Bad
const [something, setSomething] = useState(/* fake initial state */);
useEffect(() => {
setSomething(/* real initial state */)
}, []);
// Good
const [something, setSomething] = useState(/* real initial state */);
Here's a fork of your sandbox with this fix.
CodePudding user response:
This is occurring because in Home
you've created the handleOnChange
function which is passed to the List
component that is then passed to the memoized Item
component. The Item
component is kept the same across renders (and not rerendered) if the below function that you've written returns true:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked
);
}
This means that the Item
component holds the first initial version of handleOnChange
function that was created when Home
first rendered. This version of hanldeOnChange
only knows about the initial state of pageData
as it has a closure over the initial pageData
state, which is not the most up-to-date state value. You can either not memoize your Item
component, or you can change your itemPropsAreEqual
so that Item
is rerendered when your props.handleOnChange
changes:
function itemPropsAreEqual(prevItem, nextItem) {
return (
prevItem.index === nextItem.index &&
prevItem.thread === nextItem.thread &&
prevItem.checked === nextItem.checked &&
prevItem.handleOnChange === nextItem.handleOnChange // also rerender if `handleOnChange` changes.
);
}
At this point you're checking every prop passed to Item
in the comparison function, so you don't need it anymore and can just use React.memo(Item)
. However, either changing itemPropsAreEqual
alone or removing itemPropsAreEqual
from the React.memo()
call now defeats the purpose of memoizing your Item
component as handleOnChange
gets recreated every time Home
rerenders (ie: gets called). This means the above check with the new comparison function will always return false
, causing Item
to rerender each time the parent Home
component rerenders. To manage that, you can memoize handleOnChange
in the Home
component by wrapping it in a useCallback()
hook, and passing through the dependencies that it uses:
const handleOnChange = useCallback(
(iindex, id) => {
... your code in handleOnChange function ...
}
, [checkedState, pageData]); // dependencies for when a "new" version of `handleOnChange` should be created
This way, a new handleOnChange
reference is only created when needed, causing your Item
component to rerender to use the new up-to-date handleOnChange
function. There is also the useEvent()
hook which is an experimental API feature that you could look at using instead of useCallback()
(that way Item
doesn't need to rerender to deal with handleOnChange
), but that isn't available yet as of writing this (you could use it as a custom hook for the time being though by creating a shim or using alternative solutions).
See working example here.