开发者问题收集

React/Next-js:获取 TypeError:无法读取未定义的属性(读取“id”),但对象显然不为空?

2022-09-22
3361

Next-js React 复选框列表代码段提取到沙箱后,我遇到了问题。

每当我单击复选框时,我都会收到错误: TypeError:无法读取未定义的属性(读取“id”)

源自行 264setCheckedThread(prev => new Set(prev.add(pageData.currentThreads[index].id)));

但在 index.js 的顶部,我已定义静态 JSON

并且在 useEffect() 中,我使用以下内容更新 pageData 状态:

setPageData({
            currentPage:    threadsDataJSON.threads.current_page,
            currentThreads:  threadsDataJSON.threads.data,
            totalPages:     totalPages,
            totalThreads:    threadsDataJSON.threads.total,
        });

那么为什么当我单击 checkbox 时它会抛出错误?

我的沙盒链接: https://codesandbox.io/s/infallible-goldberg-vfu0ve?file=/pages/index.js

2个回答

看起来您的第 280 行上的 useEffect 仅在您选中某个框后才会触发(出于某种原因),因此在触发该 useEffect 之前, pageData.currentThreads 仍为空,这就是您遇到的错误的来源。

我建议将所有状态初始化从 useEffect 移到 useState 调用本身。例如

// Bad
const [something, setSomething] = useState(/* fake initial state */);

useEffect(() => {
  setSomething(/* real initial state */)
}, []);

// Good
const [something, setSomething] = useState(/* real initial state */);

这是您的沙盒的一个分支,其中包含此修复程序。

iamkneel
2022-09-22

发生这种情况的原因是,在 Home 中,您创建了 handleOnChange 函数,该函数被传递给 List 组件,然后传递给记忆化的 Item 组件。如果您编写的以下函数返回 true,则 Item 组件在渲染过程中保持不变(不会重新渲染):

function itemPropsAreEqual(prevItem, nextItem) {
  return (
    prevItem.index === nextItem.index &&
    prevItem.thread === nextItem.thread &&
    prevItem.checked === nextItem.checked
  );
}

这意味着 Item 组件保存了 Home 首次渲染时创建的 handleOnChange 函数的第一个初始版本。此版本的 hanldeOnChange 仅知道 pageData 的初始状态,因为它对初始 pageData 状态有一个闭包,而这不是最新的状态值。您可以不记忆 Item 组件,也可以更改 itemPropsAreEqual ,以便在 props.handleOnChange 更改时重新渲染 Item

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

此时,您正在检查比较函数中传递给 Item 的每个 prop,因此您不再需要它,只需使用 React.memo(Item) 即可。但是,单独更改 itemPropsAreEqual 或从 React.memo() 调用中删除 itemPropsAreEqual 现在违背了记忆 Item 组件的目的,因为 handleOnChange 每次 Home 重新渲染时都会重新创建(即被调用)。这意味着使用新比较函数进行的上述检查将始终返回 false ,导致 Item 在每次父 Home 组件重新渲染时重新渲染。要管理该问题,您可以在 Home 组件中记忆 handleOnChange ,方法是将其包装在 useCallback() 钩子中,并传递它使用的依赖项:

const handleOnChange = useCallback(
    (iindex, id) => {
      ... your code in handleOnChange function ...
    }
, [checkedState, pageData]); // dependencies for when a "new" version of `handleOnChange` should be created

这样,仅在需要时创建新的 handleOnChange 引用,导致您的 Item 组件重新渲染以使用新的最新的 handleOnChange 函数。还有 useEvent() 钩子,这是一个实验性的 API 功能,您可以考虑使用它来代替 useCallback() (这样 Item 就不需要重新渲染来处理 handleOnChange ),但在撰写本文时尚未可用(不过,您可以通过 创建垫片或使用替代解决方案 暂时将其用作自定义钩子)。

请参阅 此处 的工作示例。

Nick Parsons
2022-09-22