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

dar*_*urf 6 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>
  );
}
Run Code Online (Sandbox Code Playgroud)

该组件items在挂载时获取一些并将其保存到状态。

组件接收一个itemId道具(来自React Router)。

每当props.itemId更改时,我都希望它触发一个效果,在这种情况下,将其记录到控制台。


问题在于,由于效果还取决于items,因此只要items更改(例如,items通过按按钮重新获取),效果也将运行。

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


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


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

Bra*_*ams 10

?? 注意:这个答案目前是不正确的,可能会导致意外的错误/副作用。该useCallback变量需要是useEffect钩子的依赖项,因此会导致与 OP 面临的问题相同的问题。

我会尽快解决

最近在一个项目中遇到了这个问题,我们的解决方案是将 的内容移动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>
  );
}
Run Code Online (Sandbox Code Playgroud)

所以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 ]) 
Run Code Online (Sandbox Code Playgroud)

  • 对不起,伙计,这是不对的。`onItemIdChange` (函数)需要声明为 `useEffect` 的依赖项。这会使这段代码对于OP的问题无效。 (2认同)
  • 是的,@GunarGessner 是正确的,我可能在发布这个答案后不久“发现”了这一点(当时对钩子来说是相当新的),使用 `useCallback` 创建的方法_应该_确实被定义为 `useEffect` 钩子的依赖项。我会更新我的答案 (2认同)
  • @BradAdams,如果您删除这个答案,那就太好了,因为它可能会导致人们养成坏习惯!或者至少在编辑时注明这不是可接受的答案。 (2认同)

Dav*_*oas 9

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>
  );
}
Run Code Online (Sandbox Code Playgroud)

我认为这可能对您有帮助。

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

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


Gun*_*ner 7

一个简单的方法是编写一个自定义钩子来帮助我们解决这个问题

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

// Helper hook
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}
Run Code Online (Sandbox Code Playgroud)

然后在 useEffect

function Component(props) {
  const hasItemIdChanged = useCompare(props.itemId);
  useEffect(() => {
    if(hasItemIdChanged) {
      // …
    }
  }, [hasItemIdChanged])
  return <></>
}
Run Code Online (Sandbox Code Playgroud)

  • @GiorgioTempesta 你说得完全正确。我已经进行了一些测试并确认了您的修复。我现在已经相应地修复了我的答案中的代码。谢谢。 (3认同)

Gyu*_*Fox 6

2022年答案

我知道这是一个老问题,但仍然值得回答。效果今年受到了很多关注,现在比以前更清楚应该/不应该对效果做什么。新的 React 文档仍处于测试阶段,详细介绍了该主题。

因此,在回答“如何”之前,您必须首先回答“为什么”,因为实际上并没有很多真正的用例仅在某些依赖项发生变化时才运行效果。

您可能不需要效果

您可能不需要效果”实际上是页面标题,表明您可能出于错误的原因使用效果。你给我们的例子实际上是在“当道具改变时调整某些状态”中讨论的

您的代码可以简单地重写,而不会产生第二种效果:

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();
  }, [handleFetchItems]);

  // Find the item that matches in the list
  const item = items.find(item => item.id === props.itemId);
  
  return (
    <Button onClick={handleFetchItems}>Fetch items</Button>
  );
}
Run Code Online (Sandbox Code Playgroud)

事实上,您不需要仅仅为了识别列表中的项目而使用效果。React 已经在变化时重新渲染items,因此item可以直接在组件中重新计算。

注意:如果您确实想在项目更改时登录到控制台,您仍然可以将此代码添加到组件中:

  useEffect(() => {
    console.log("Item changed to ", item?.name);
  }, [item])
Run Code Online (Sandbox Code Playgroud)

请注意,howitem是效果的唯一依赖项,因此它仅在item更改时运行。但这并不是再次产生效果的良好用例。

相反,如果您想要执行某些操作来响应应用程序中发生的事件,您应该这样做

将事件与效果分开

是的,“将事件与效果分离”也是新 React 文档中页面的标题!在您给我们的示例中,您希望在发生变化时写入控制台item。但我并不天真,我知道你在实践中可能想做的不仅仅是这些。

如果你想在其他事情发生变化useEvent做一些事情,那么你可以使用新的钩子为其创建一个事件处理程序(或者至少使用它的polyfill,因为它尚未发布)。这里讨论的事件是itemId道具改变。

然后您可以将第二个效果更改为:

// Handle a change of item ID
const onItemIdChange = useEvent((itemId) => {
  const item = items?.find(item => item.id === itemId);
  console.log("Item changed to " item.name);
});

// Trigger onItemIdChange when item ID changes
useEffect(() => {
  onItemIdChange(props.itemId);
}, [props.itemId]);
Run Code Online (Sandbox Code Playgroud)

此方法的优点是useEvent返回一个稳定的函数,因此您不需要将其添加为效果的依赖项(它永远不会改变)。此外,您传递给的函数useEvent没有依赖项数组:它可以访问组件内的所有属性、状态或变量,并且它始终是新鲜的(没有过时的值)。

我仍然认为,根据您给我们提供的示例,您实际上并不需要效果。但对于其他真实情况,至少您现在知道如何从效果中提取事件,因此您可以避免破解依赖项数组。


Joh*_*ard 5

我是一个 react hooks 初学者,所以这可能不对,但我最终为这种场景定义了一个自定义钩子:

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
  );
}
Run Code Online (Sandbox Code Playgroud)

它监视第二个依赖项数组(可能少于 useEffect 依赖项)的更改并在这些更改中有任何更改时生成原始 useEffect。

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

// 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])
Run Code Online (Sandbox Code Playgroud)

这是它的一个简化示例, useEffectWhen 只会在 id 更改时显示在控制台中,而不是 useEffect 在项目或 id 更改时记录。

这将在没有任何 eslint 警告的情况下工作,但这主要是因为它混淆了 exlint-deps 的 eslint 规则!如果你想确保你有你需要的 deps,你可以在 eslint 规则中包含 useEffectWhen。您将需要在 package.json 中使用它:

"eslintConfig": {
  "extends": "react-app",
  "rules": {
    "react-hooks/exhaustive-deps": [
      "warn",
      {
        "additionalHooks": "useEffectWhen"
      }
    ]
  }
},
Run Code Online (Sandbox Code Playgroud)

并且可以选择在您的 .env 文件中使用 react-scripts 来获取它:

EXTEND_ESLINT=true
Run Code Online (Sandbox Code Playgroud)