Home > Net >  React/Next-js : Getting TypeError: Cannot read properties of undefined (reading 'id'), but
React/Next-js : Getting TypeError: Cannot read properties of undefined (reading 'id'), but

Time:09-22

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.

  • Related