React:在 useEffect 中调用上下文函数时防止无限循环

Posted

技术标签:

【中文标题】React:在 useEffect 中调用上下文函数时防止无限循环【英文标题】:React: Prevent infinite Loop when calling context-functions in useEffect 【发布时间】:2021-01-15 20:52:02 【问题描述】:

在我的 react 应用程序中,我正在渲染 <Item> 组件的不同实例,我希望它们在上下文中注册/取消注册,具体取决于它们当前是否已安装。

我正在使用两个上下文(ItemContext 提供注册项目,ItemContextSetters 提供注册/注销功能)。

const ItemContext = React.createContext();
const ItemContextSetters = React.createContext(
  registerItem: (id, data) => undefined,
  unregisterItem: (id) => undefined
);

function ContextController(props) 
  const [items, setItems] = useState();

  const unregisterItem = useCallback(
    (id) => 
      const itemsUpdate =  ...items ;
      delete itemsUpdate[id];
      setItems(itemsUpdate);
    ,
    [items]
  );

  const registerItem = useCallback(
    (id, data) => 
      if (items.hasOwnProperty(id) && items[id] === data) 
        return;
      

      const itemsUpdate =  ...items, [id]: data ;
      setItems(itemsUpdate);
    ,
    [items]
  );

  return (
    <ItemContext.Provider value= items >
      <ItemContextSetters.Provider value= registerItem, unregisterItem >
        props.children
      </ItemContextSetters.Provider>
    </ItemContext.Provider>
  );
 

&lt;Item&gt; 组件应在挂载时自行注册,或者其props.data 更改并在卸载时取消注册。所以我认为使用useEffect 可以非常干净地完成:

function Item(props) 
  const itemContextSetters = useContext(ItemContextSetters);

  useEffect(() => 
    itemContextSetters.registerItem(props.id, props.data);

    return () => 
      itemContextSetters.unregisterItem(props.id);
    ;
  , [itemContextSetters, props.id, props.data]);

  ...

完整示例见codesandbox

现在,问题是这给了我一个无限循环,我不知道如何做得更好。循环是这样发生的(我相信):

&lt;Item&gt; 呼叫 registerItem 在上下文中,items 被更改,因此registerItem 被重新构建(因为它依赖于[items] 这会触发&lt;Item&gt; 中的更改,因为itemContextSetters 已更改并且useEffect 会再次执行。 还执行了之前渲染的清理效果! (如here 所述:“React 还会在下次运行效果之前清理上一次渲染中的效果”) 这又在上下文中改变了items 等等……

我真的想不出一个可以避免这个问题的干净解决方案。我是否滥用了任何钩子或上下文 api?您能否帮助我了解有关如何编写组件在其useEffect-body 和useEffect-cleanup 中调用的此类注册/注销上下文的一般模式?

我不想做的事情:

完全摆脱上下文。在我的真实应用中,结构更复杂,整个应用中的不同组件都需要这些信息,所以我相信我想坚持一个上下文 避免从 dom 中硬删除 &lt;Item&gt; 组件(使用 renderFirstBlock &amp;&amp; )并改用类似状态 isHidden 的东西。在我的真实应用程序中,目前我无法更改。我的目标是跟踪所有现有组件实例的data

谢谢!

【问题讨论】:

我猜itemContextSetters 是你的useContext(ItemContextSetters) 的名字。如果是这种情况,则不应将其作为 useEffect 依赖项的一部分。 这会导致 linting 错误:React Hook useEffect has a missing dependency: 'itemContextSetters'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)。并且有充分的理由!由于效果是使用 itemContextSetters 必须确保拥有最新版本 不一定,你可以抑制那个错误。 是的,但该错误的出现是有充分理由的。它提醒我,如果我使用不是最新版本的itemContextSetters,我会引入错误。 (由于它是用useCallback 构建的,并且依赖于[items],所以每次items 更改时都会有一个新版本) 鉴于您的问题,您所追求的是安装组件的时间。不需要依赖项。 【参考方案1】:

你可以让你的 setter 有稳定的引用,因为他们不需要依赖items

const ItemContext = React.createContext();
const ItemContextSetters = React.createContext(
  registerItem: (id, data) => undefined,
  unregisterItem: (id) => undefined
);

function ContextController(props) 
  const [items, setItems] = useState();

  const unregisterItem = useCallback(
    (id) => 
      setItems(currentItems => 
        const itemsUpdate =  ...currentItems ;
        delete itemsUpdate[id];
        return itemsUpdate
      );
    ,
    []
  );

  const registerItem = useCallback(
    (id, data) => 

      setItems(currentItems => 
        if (currentItems.hasOwnProperty(id) && currentItems[id] === data) 
          return currentItems;
        
        return  ...currentItems, [id]: data 
       );
    ,
    []
  );
  
  const itemsSetters = useMemo(() => ( registerItem, unregisterItem ), [registerItem, unregisterItem])

  return (
    <ItemContext.Provider value= items >
      <ItemContextSetters.Provider value=itemsSetters>
        props.children
      </ItemContextSetters.Provider>
    </ItemContext.Provider>
  );

现在你的效果应该可以正常工作了

【讨论】:

我想提一下,您更新items 对象的方法是正确的方法。 OP 在他的 renderFirstBlock 切换中也犯了一个错误。应该是onClick=() =&gt; setRenderFirstBlock(prev =&gt; !prev) 太好了,谢谢!这正是我所需要的!到目前为止,我不知何故错过了我可以将函数传递给状态设置器,从而避免对当前状态的依赖。这也将使我的许多其他事情变得更容易,谢谢!但是我在解决方案中很难理解一件事:为什么我需要useMemo?对我来说,这行代码如下:如果有两个更改之一,请更新 registerItemunregisterItem。这有什么帮助? 它可以防止上下文值在您调用setItems 时发生变化。如果没有这个,包含registerItemunregisterItem 的对象将在每次渲染时创建 我虽然 useCallback 会确保 registerItem 仅在依赖项(为空)更改时才重新创建? (但是是的,当我尝试它时,我可以明确地看到没有 useMemo 它不起作用) 我想我明白了:D 我们将itemSetters 作为一个单元进行记忆。在渲染函数中,registerItem, unregisterItem 将表达一个新对象(在我们的 Item 组件中触发 useEffect),即使 registerItemunregisterItem 没有改变。非常感谢您的时间和帮助!

以上是关于React:在 useEffect 中调用上下文函数时防止无限循环的主要内容,如果未能解决你的问题,请参考以下文章

React:使用 Refs 修复缺少的依赖警告 useEffect

React/React Context API:使用 useEffect() 钩子时等待上下文值

如果它们的状态在 React useEffect 中发生变化,如何区分上下文值

React Hook useEffect 缺少依赖项(在上下文中定义的函数)

React 中带有 useEffect 的异步上下文

在函数内反应 useEffect 陈旧值