简体   繁体   English

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

[英]React: Prevent infinite Loop when calling context-functions in useEffect

In my react app, I am rendering different instances of <Item> Components and I want them to register/unregister in a Context depending if they are currently mounted or not.在我的 React 应用程序中,我正在渲染<Item>组件的不同实例,我希望它们在上下文中注册/取消注册,具体取决于它们当前是否已安装。

I am doing this with two Contexts ( ItemContext provides the registered items, ItemContextSetters provides the functions to register/unregister).我正在使用两个上下文( 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>
  );
} 

The <Item> Components should register themselves when they are mounted or their props.data changes and unregister when they are unmounted. <Item>组件应该在挂载或props.data更改时注册自己,并在卸载时取消注册。 So I thought that could be done very cleanly with useEffect :所以我认为可以用useEffect非常干净地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]);

  ...
}

Full example see this codesandbox完整示例请参阅此代码和框

Now, the problem is that this gives me an infinite loop and I don't know how to do it better.现在,问题是这给了我一个无限循环,我不知道如何做得更好。 The loop is happening like this (I believe):循环是这样发生的(我相信):

  • An <Item> calls registerItem <Item>调用registerItem
  • In the Context, items is changed and therefore registerItem is re-built (because it depends on [items]在 Context 中, items被改变,因此registerItem被重新构建(因为它依赖于[items]
  • This triggers a change in <Item> because itemContextSetters has changed and useEffect is executed again.这会触发<Item>的更改,因为itemContextSetters已更改并且再次执行useEffect
  • Also the cleanup effect from the previous render is executed!还执行了先前渲染的清理效果! (As stated here : "React also cleans up effects from the previous render before running the effects next time") (如前所述这里:“响应也从以前的渲染清理运行的影响下一次前效应”)
  • This again changes items in the context这再次更改了上下文中的items
  • And so on ...等等 ...

I really can't think of a clean solution that avoids this problem.我真的想不出一个干净的解决方案来避免这个问题。 Am I misusing any hook or the context api?我是否滥用了任何钩子或上下文 api? Can you help me with any general pattern on how write such a register/unregister Context that is called by Components in their useEffect -body and useEffect -cleanup?关于如何编写由组件在其useEffectuseEffect -cleanup 中调用的注册/取消注册上下文的任何一般模式,您能帮我吗?

Things I'd prefer not to do:我不想做的事情:

  • Getting rid of the context altogether.完全摆脱上下文。 In my real App, the structure is more complicated and different components throughout the App need this information so I believe I want to stick to a context在我的真实应用中,结构更复杂,整个应用中的不同组件都需要这些信息,所以我相信我想坚持一个上下文
  • Avoiding the hard removal of the <Item> components from the dom (with {renderFirstBlock && ) and use something like a state isHidden instead.避免从 dom 中硬删除<Item>组件(使用{renderFirstBlock && )并使用类似状态isHidden东西。 In my real App this is currently nothing I can change.在我的真实应用程序中,目前我无法改变。 My goal is to track data of all existing component instances.我的目标是跟踪data所有现有组件实例。

Thank you!谢谢!

You can make your setters have stable references, as they don't really need to be dependant on items :你可以让你的 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>
  );
}

Now your effect should work as expected现在您的效果应该按预期工作

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM