仅在效果的一个部门发生变化时反应useEffect Hook

时间:2019-04-17 09:44:55

标签: javascript reactjs react-hooks

我有一个使用Hooks的功能组件:

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

  // In a callback Hook to prevent unnecessary re-renders 
  const handleFetchItems = useCallback(() => {
    fetchItemsFromApi().then(setItems);
  }, []);

  // Fetch items on mount
  useEffect(() => {
    handleFetchItems();
  }, []);

  // I want this effect to run only when 'props.itemId' changes,
  // not when 'items' changes
  useEffect(() => {
    if (items) {
      const item = items.find(item => item.id === props.itemId);
      console.log("Item changed to " item.name);
    }
  }, [ items, props.itemId ])

  // Clicking the button should NOT log anything to console
  return (
    <Button onClick={handleFetchItems}>Fetch items</Button>
  );
}

该组件会在安装时获取一些items并将其保存到状态。

该组件从React Router接收一个itemId道具。

只要props.itemId发生变化,我都希望它触发一个效果,在这种情况下,将其记录到控制台。


问题在于,由于效果还取决于items,因此只要items发生变化,效果也将运行,例如,通过按按钮。

可以通过将前一个items存储在一个单独的状态变量中并进行比较来解决此问题,但这似乎很简单,并且增加了样板。通过使用组件类,可以通过比较props.itemId中的当前和以前的道具来解决,但是使用功能组件是不可能的,这是使用Hooks的要求。


仅当其中一个参数发生更改时,触发取决于多个参数的效果的最佳方法是什么?


PS。挂钩是一种新事物,我认为我们都在尽力找出如何正确使用挂钩的功能,因此,如果我的想法对您来说似乎是错误的或尴尬的,请指出。

6 个答案:

答案 0 :(得分:3)

最近在一个项目中遇到了这个问题,我们的解决方案是将useEffect的内容移动到回调(在这种情况下是已记忆)中,并调整两者的依赖关系。使用您提供的代码,它看起来像这样:

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

  const onItemIdChange = useCallback(() => {
    if (items) {
      const item = items.find(item => item.id === props.itemId);
      console.log("Item changed to " item.name);
    }
  }, [items, props.itemId]);

  // I want this effect to run only when 'props.itemId' changes,
  // not when 'items' changes
  useEffect(onItemIdChange, [ props.itemId ]);

  // Clicking the button should NOT log anything to console
  return (
    <Button onClick={handleFetchItems}>Fetch items</Button>
  );
}

因此useEffect仅以ID属性作为其依赖项,并回调ID的项目

实际上,您可以从回调中删除ID依赖项,并将其作为参数传递给onItemIdChange回调:

const onItemIdChange = useCallback((id) => {
  if (items) {
    const item = items.find(item => item.id === id);
    console.log("Item changed to " item.name);
  }
}, [items]);

useEffect(() => {
  onItemIdChange(props.itemId)
}, [ props.itemId ]) 

答案 1 :(得分:1)

React小组说,获取prev值的最佳方法是使用useRef:https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state

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

  const prevItemIdRef = useRef();
  useEffect(() => {
    prevItemIdRef.current = props.itemId;
  });
  const prevItemId = prevItemIdRef.current;

  // In a callback Hook to prevent unnecessary re-renders 
  const handleFetchItems = useCallback(() => {
    fetchItemsFromApi().then(setItems);
  }, []);

  // Fetch items on mount
  useEffect(() => {
    handleFetchItems();
  }, []);

  // I want this effect to run only when 'props.itemId' changes,
  // not when 'items' changes
  useEffect(() => {
    if(prevItemId !== props.itemId) {
      console.log('diff itemId');
    }

    if (items) {
      const item = items.find(item => item.id === props.itemId);
      console.log("Item changed to " item.name);
    }
  }, [ items, props.itemId ])

  // Clicking the button should NOT log anything to console
  return (
    <Button onClick={handleFetchItems}>Fetch items</Button>
  );
}

我认为这对您有帮助。

注意:如果不需要以前的值,另一种方法是为props.itemId写一个useEffect more

React.useEffect(() => {
  console.log('track changes for itemId');
}, [props.itemId]);

答案 2 :(得分:1)

一种简单的方法是编写一个自定义钩子来帮助我们

// Desired hook
const useCompare = (val) => {
    const prevVal = usePrevious(val)
    return prevVal !== val
}

// Helper hook
const usePrevious = (value) => {
    const ref = useRef();
    useEffect(() => {
      ref.current = value;
    });
    return ref.current;
}

然后在useEffect

中使用它
const Component = (props) => {
    const hasItemIdChanged = useCompare(props.itemId);
    useEffect(() => {
        if(hasItemIdChanged) {
         // ...
        }
    }, [items, props.itemId])
}

答案 3 :(得分:0)

在提供的示例中,您的效果不取决于itemsitemId,而是取决于集合中的一项。

是的,您需要itemsitemId才能获得该项目,但这并不意味着您必须在依赖项数组中指定它们。

为确保仅在目标项目更改时才执行该项目,应使用相同的查找逻辑将该项目传递给依赖项数组。

useEffect(() => {
  if (items) {
    const item = items.find(item => item.id === props.itemId);
    console.log("Item changed to " item.name);
  }
}, [ items.find(item => item.id === props.itemId) ])

答案 4 :(得分:0)

我只是自己尝试过一次,在我看来,您不需要将它们放入useEffect依赖项列表中就可以拥有更新的版本。意味着您可以只放入props.itemId,仍然在效果中使用items

我在这里创建了一个片段来尝试证明/说明这一点。让我知道是否有问题。

const Child = React.memo(props => {
  const [items, setItems] = React.useState([]);
  const fetchItems = () => {
    setTimeout(() => {
      setItems((old) => {
        const newItems = [];
        for (let i = 0; i < old.length + 1; i++) {
          newItems.push(i);
        }
        return newItems;
      })
    }, 1000);
  }
  
  React.useEffect(() => {
    console.log('OLD (logs on both buttons) id:', props.id, 'items:', items.length);
  }, [props.id, items]);
  
  React.useEffect(() => {
    console.log('NEW (logs on only the red button) id:', props.id, 'items:', items.length);
  }, [props.id]);

  return (
    <div
      onClick={fetchItems}
      style={{
        width: "200px",
        height: "100px",
        marginTop: "12px",
        backgroundColor: 'orange',
        textAlign: "center"
      }}
    >
      Click me to add a new item!
    </div>
  );
});

const Example = () => {
  const [id, setId] = React.useState(0);

  const updateId = React.useCallback(() => {
    setId(old => old + 1);
  }, []);

  return (
    <div style={{ display: "flex", flexDirection: "row" }}>
      <Child
        id={id}
      />
      <div
        onClick={updateId}
        style={{
          width: "200px",
          height: "100px",
          marginTop: "12px",
          backgroundColor: 'red',
          textAlign: "center"
        }}
      >Click me to update the id</div>
    </div>
  );
};

ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

答案 5 :(得分:0)

我是React hooks的初学者,所以可能不对,但是我最终为这种情况定义了一个自定义钩子。

query GetMyModel(id: ID!){
    getMyModel(id: $id){
        someProperty
    }
}

它监视第二个依赖项数组(可以小于useEffect依赖项)以查找更改,并在这些更改中的任何一个产生原始的useEffect。

以下是在示例中如何使用(和重用)它而不是useEffect的方法:

const useEffectWhen = (effect, deps, whenDeps) => {
  const whenRef = useRef(whenDeps || []);
  const initial = whenRef.current === whenDeps;
  const whenDepsChanged = initial || !whenRef.current.every((w, i) => w === whenDeps[i]);
  whenRef.current = whenDeps;
  const nullDeps = deps.map(() => null);

  return useEffect(
    whenDepsChanged ? effect : () => {},
    whenDepsChanged ? deps : nullDeps
  );
}

Here's a simplified example of it in action,useEffectWhen仅在id更改时显示在控制台中,而useEffect则在项目或ID更改时记录日志。

这将在没有任何附带警告的情况下起作用,但这主要是因为它混淆了详尽说明的附带规则!如果要确保您拥有所需的部门,可以在eslint规则中包括useEffectWhen。您将在package.json中使用它:

// I want this effect to run only when 'props.itemId' changes,
// not when 'items' changes
useEffectWhen(() => {
  if (items) {
    const item = items.find(item => item.id === props.itemId);
    console.log("Item changed to " item.name);
  }
}, [ items, props.itemId ], [props.itemId])

,也可以选择在您的.env文件中将此文件供react-scripts提取:

"eslintConfig": {
  "extends": "react-app",
  "rules": {
    "react-hooks/exhaustive-deps": [
      "warn",
      {
        "additionalHooks": "useEffectWhen"
      }
    ]
  }
},