【问题标题】:useEffect when all dependencies have changed?当所有依赖项都发生变化时使用效果?
【发布时间】:2020-11-08 11:37:45
【问题描述】:

目前useEffect 仅在其中一个依赖项发生更改时被触发。

当两个(或所有)依赖项都发生变化时,我如何更新它/使用它来回火?

【问题讨论】:

  • 你能详细说明你所说的“开火”是什么意思吗? useCallback 只是一个记忆回调函数。
  • 对不起,我把它和 useEffect 混在一起了,每次参数改变时都会触发? Firing 是指调用您在第一个参数中提供的回调。
  • 我认为通过useEffect API 是不可能的。
  • 这是一个非常简单的问题,但我可以解决它。目前我使用 redux 来设置一个选项。这会触发刷新,然后使用新选项触发数据源的刷新。这将导致 useEffect 触发,因为两个选项都已更改并且新数据已更改。您可能认为只是在新数据上调用它,但是数据可以从其他来源更改,并且 useEffect 很昂贵,所以我只想在选项和 THEN 数据更改后才这样做。在数据不起作用之前触发它。也许我遗漏了一些明显的东西?
  • 你有 Redux 吗?那么这可能是一个错误的问题,尝试在商店中解决它。

标签: reactjs react-hooks


【解决方案1】:

当所有依赖项都发生变化时,您需要添加一些逻辑来调用您的效果。这是useEffectAllDepsChange,应该可以实现您想要的行为。

这里的策略是将之前的部门与当前的部门进行比较。如果它们不完全不同,我们将之前的 deps 保存在 ref 中,直到它们不同时才更新它。这允许您在调用效果之前多次更改 deps。

import React, { useEffect, useState, useRef } from "react";

// taken from https://usehooks.com/usePrevious/
function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

function useEffectAllDepsChange(fn, deps) {
  const prevDeps = usePrevious(deps);
  const changeTarget = useRef();

  useEffect(() => {
    // nothing to compare to yet
    if (changeTarget.current === undefined) {
      changeTarget.current = prevDeps;
    }

    // we're mounting, so call the callback
    if (changeTarget.current === undefined) {
      return fn();
    }

    // make sure every dependency has changed
    if (changeTarget.current.every((dep, i) => dep !== deps[i])) {
      changeTarget.current = deps;

      return fn();
    }
  }, [fn, prevDeps, deps]);
}

export default function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);

  useEffectAllDepsChange(() => {
    console.log("running effect", [a, b]);
  }, [a, b]);

  return (
    <div>
      <button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
      <button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
    </div>
  );
}

受 Richard 启发的另一种方法更简洁,但缺点是跨更新渲染更多。

function useEffectAllDepsChange(fn, deps) {
  const [changeTarget, setChangeTarget] = useState(deps);

  useEffect(() => {
    setChangeTarget(prev => {
      if (prev.every((dep, i) => dep !== deps[i])) {
        return deps;
      }

      return prev;
    });
  }, [deps]);

  useEffect(fn, changeTarget);
}

【讨论】:

  • 我喜欢这个,因为它对于给定的所有依赖项都是动态的。
  • 我喜欢它,对我的回答进行了很好的改进。更多渲染?不确定。
  • 第二种方法更简洁。
【解决方案2】:

我喜欢@AustinBrunkhorst 的灵魂,但你可以用更少的代码来做到这一点。

使用仅在满足您的条件时更新的状态对象,并将其设置在第二个 useEffect 中。

import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
  const [ab, setAB] = useState({a, b});

  useEffect(() => {
    setAB(prev => {
      console.log('prev AB', prev)
      return (a !== prev.a && b !== prev.b) 
        ? {a,b} 
        : prev;  // do nothing
    })
  }, [a, b])

  useEffect(() => {
    console.log('both have changed')
  }, [ab])

  return (
    <div className="App">
      <div>Click on a button to increment its value.</div>
      <button onClick={() => setA((prev) => prev + 1)}>A: {a}</button>
      <button onClick={() => setB((prev) => prev + 1)}>B: {b}</button>
    </div>
  );
}

【讨论】:

  • 不错的方法。我认为这里的缺点是setAB 引起的额外渲染。我们还可以通过将此功能移至与useEffect 具有相同签名的另一个挂钩来提高可重用性。
【解决方案3】:

为了演示如何以各种方式编写钩子,这是我的方法。这不会在初始属性中调用效果。

import React, { useEffect, useRef, useState } from "react";
import "./styles.css";

function usePrevious(state) {
  const ref = useRef();

  useEffect(() => {
    ref.current = state;
  });

  return ref.current;
}

function useAllChanged(callback, array) {
  const previousArray = usePrevious(array);

  console.log("useAllChanged", array, previousArray);

  if (previousArray === undefined) return;

  const allChanged = array.every((state, index) => {
    const previous = previousArray[index];
    return previous !== state;
  });

  if (allChanged) {
    callback(array, previousArray);
  }
}

const randomIncrement = () => Math.floor(Math.random() * 4);

export default function App() {
  const [state1, setState1] = useState(0);
  const [state2, setState2] = useState(0);
  const [state3, setState3] = useState(0);

  useAllChanged(
    (state, prev) => {
      alert("Everything changed!");
      console.info(state, prev);
    },
    [state1, state2, state3]
  );

  const onClick = () => {
    console.info("onClick");
    setState1(state => state + randomIncrement());
    setState2(state => state + randomIncrement());
    setState3(state => state + randomIncrement());
  };

  return (
    <div className="App">
      <p>State 1: {state1}</p>
      <p>State 2: {state2}</p>
      <p>State 3: {state3}</p>

      <button onClick={onClick}>Randomly increment</button>
    </div>
  );
}

【讨论】:

    【解决方案4】:

    您必须跟踪依赖项的先前值,并检查是否只有其中一个发生了变化,或者两者/全部发生了变化。基本实现可能如下所示:

    import React from "react";
    
    const usePrev = value => {
      const ref = React.useRef();
    
      React.useEffect(() => {
        ref.current = value;
      }, [value]);
    
      return ref.current;
    };
    
    const App = () => {
      const [foo, setFoo] = React.useState(0);
      const [bar, setBar] = React.useState(0);
      const prevFoo = usePrev(foo);
      const prevBar = usePrev(bar);
    
      React.useEffect(() => {
        if (prevFoo !== foo && prevBar !== bar) {
          console.log("both foo and bar changed!");
        }
      }, [prevFoo, prevBar, foo, bar]);
    
      return (
        <div className="App">
          <h2>foo: {foo}</h2>
          <h2>bar: {bar}</h2>
          <button onClick={() => setFoo(v => v + 1)}>Increment foo</button>
          <button onClick={() => setBar(v => v + 1)}>Increment bar</button>
          <button
            onClick={() => {
              setFoo(v => v + 1);
              setBar(v => v + 1);
            }}
          >
            Increment both
          </button>
        </div>
      );
    };
    
    export default App;
    
    

    这里还有一个CodeSandbox link可以玩。

    您可以检查usePrev 挂钩在其他地方的工作方式,例如here

    【讨论】:

    • prevFoo 会在您单击“增量栏”时更新?
    • 不,点击“增量栏”时不会更新
    • 添加&lt;h2&gt;prevFoo: {prevFoo}&lt;/h2&gt;你会看到它发生。
    • 已添加,如果我只点击“增量栏”prevFoo 保持不变。当您单击“递增 foo”或“同时递增”,然后单击“递增栏”时,它会发生变化,但这就是它的目的,它会延迟更改。
    • @Maxim 看起来您忘记重新加载页面 - prevFoo 开始 undefined 然后在“增量栏”时获取 0。然后,如果您单击“增加 foo”,您不会看到“foo 和 bar 都已更改”,这是其他解决方案的行为。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-29
    • 2010-09-19
    • 1970-01-01
    • 2019-11-14
    • 1970-01-01
    相关资源
    最近更新 更多