我可以在 useEffect 挂钩中设置状态吗

Can I set state inside a useEffect hook

假设我有一些状态依赖于其他一些状态(例如,当 A 改变时我希望 B 改变)。

在 useEffect 钩子中创建一个观察 A 并设置 B 的钩子是否合适?

效果是否会级联,当我单击按钮时,第一个效果会触发,导致 b 发生变化,导致第二个效果在下一次渲染之前触发?像这样构建代码有任何性能缺点吗?

let MyComponent = props => {
  let [a, setA] = useState(1)
  let [b, setB] = useState(2)
  useEffect(
    () => {
      if (/*some stuff is true*/) {
        setB(3)
      }
    },
    [a],
  )
  useEffect(
    () => {
      // do some stuff
    },
    [b],
  )

  return (
    <button
      onClick={() => {
        setA(5)
      }}
    >
      click me
    </button>
  )
}

效果总是在渲染阶段完成后执行,即使您在一个效果中设置状态,另一个效果将读取更新的状态并仅在渲染阶段后对其执行操作。

话虽如此,但最好是同时采取两种行动,除非 b 可能会因 changing a 以外的原因而发生变化,在这种情况下,您也希望这样做执行相同的逻辑

一般来说,在 useEffect 中使用 setState 会创建一个您很可能不希望导致的无限循环。该规则有几个例外情况,我稍后会介绍。

useEffect 在每次渲染后调用,当在其中使用 setState 时,它将导致组件重新渲染,这将调用 useEffect 等等上。

useEffect 中使用 useState 不会导致无限循环的常见情况之一是将空数组作为第二个参数传递给 useEffect,例如 useEffect(() => {....}, []) 这意味着效果函数应该被调用一次:仅在第一个 mount/render 之后。当您在组件中进行数据获取并且希望将请求数据保存在组件状态时,这被广泛使用。

useEffect可以挂在某个道具或状态上。所以,为了避免无限循环挂钩,你需要做的是绑定一些变量或状态来生效

例如:

useEffect(myeffectCallback, [])

以上效果只会在组件渲染后触发。这类似于 componentDidMount 生命周期

const [something, setSomething] = withState(0)
const [myState, setMyState] = withState(0)
useEffect(() => {
  setSomething(0)
}, myState)

以上效果只会触发我的状态已经改变这类似于 componentDidUpdate 除了不是每个改变的状态都会触发它。

您可以通过此阅读更多详细信息link

为了将来的目的,这也可能有帮助:

useEffect 中使用 setState 是可以的,您只需要注意已经描述的不要创建循环。

但这不是唯一可能出现的问题。见下文:

假设您有一个组件 Comp,它从父组件接收 props,并且根据 props 更改您想要设置 Comp 的状态。出于某种原因,您需要为每个道具更改不同的 useEffect:

不要这样做

useEffect(() => {
  setState({ ...state, a: props.a });
}, [props.a]);

useEffect(() => {
  setState({ ...state, b: props.b });
}, [props.b]);

它可能永远不会改变 a 的状态,如您在本例中所见: https://codesandbox.io/s/confident-lederberg-dtx7w

在这个例子中发生这种情况的原因是 当您同时更改 prop.a 和 [=19] 时, 在同一个反应周期中 都使用了 Effects 运行 =] 所以当你做 setState{...state} 的值在两个 useEffect 中完全相同,因为它们在相同的上下文中。当你 运行 第二个 setState 它将取代第一个 setState.

改为这样做

这个问题的解决方案基本上是这样调用setState

useEffect(() => {
  setState(state => ({ ...state, a: props.a }));
}, [props.a]);

useEffect(() => {
  setState(state => ({ ...state, b: props.b }));
}, [props.b]);

在这里查看解决方案:https://codesandbox.io/s/mutable-surf-nynlx

现在,当您继续 setState

时,您总是会收到最新和正确的状态值

我希望这对某人有所帮助!

▶ 1. 我可以在 useEffect 挂钩中设置状态吗?

原则上,您可以在需要的地方自由设置状态 - 包括 useEffecteven during rendering 内部。只需确保通过有条件地正确设置 Hook deps and/or 状态来避免无限循环。


▶ 2. 假设我有一些状态依赖于其他状态。创建一个钩子来观察 A 并在 useEffect 钩子中设置 B 是否合适?

您刚刚描述了 useReducer 的经典用例:

useReducer is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. (React docs)

When setting a state variable depends on the current value of another state variable, you might want to try replacing them both with useReducer. [...] When you find yourself writing setSomething(something => ...), it’s a good time to consider using a reducer instead. (Dan Abramov, Overreacted blog)

let MyComponent = () => {
  let [state, dispatch] = useReducer(reducer, { a: 1, b: 2 });

  useEffect(() => {
    console.log("Some effect with B");
  }, [state.b]);

  return (
    <div>
      <p>A: {state.a}, B: {state.b}</p>
      <button onClick={() => dispatch({ type: "SET_A", payload: 5 })}>
        Set A to 5 and Check B
      </button>
      <button onClick={() => dispatch({ type: "INCREMENT_B" })}>
        Increment B
      </button>
    </div>
  );
};

// B depends on A. If B >= A, then reset B to 1.
function reducer(state, { type, payload }) {
  const someCondition = state.b >= state.a;

  if (type === "SET_A")
    return someCondition ? { a: payload, b: 1 } : { ...state, a: payload };
  else if (type === "INCREMENT_B") return { ...state, b: state.b + 1 };
  return state;
}

ReactDOM.render(<MyComponent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect } = React</script>


▶ 3. 效果会级联吗,当我单击按钮时,第一个效果会触发,导致 b 发生变化,导致第二个效果在下一次渲染之前触发?

useEffect always 运行s after 提交渲染并应用 DOM 更改。第一个效果触发,更改 b 并导致重新渲染。此渲染完成后,由于 b 更改,第二个效果将 运行。

let MyComponent = props => {
  console.log("render");
  let [a, setA] = useState(1);
  let [b, setB] = useState(2);

  let isFirstRender = useRef(true);

  useEffect(() => {
    console.log("useEffect a, value:", a);
    if (isFirstRender.current) isFirstRender.current = false;
    else setB(3);
    return () => {
      console.log("unmount useEffect a, value:", a);
    };
  }, [a]);
  useEffect(() => {
    console.log("useEffect b, value:", b);
    return () => {
      console.log("unmount useEffect b, value:", b);
    };
  }, [b]);

  return (
    <div>
      <p>a: {a}, b: {b}</p>
      <button
        onClick={() => {
          console.log("Clicked!");
          setA(5);
        }}
      >
        click me
      </button>
    </div>
  );
};

ReactDOM.render(<MyComponent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>


▶ 4. 像这样构造代码是否有任何性能缺点?

是的。通过将 b 的状态更改包装在 a 的单独 useEffect 中,浏览器有一个额外的 layout/paint 阶段 - 这些效果可能对用户可见。如果没有办法尝试 useReducer,可以直接将 b 状态与 a 一起更改:

let MyComponent = () => {
  console.log("render");
  let [a, setA] = useState(1);
  let [b, setB] = useState(2);

  useEffect(() => {
    console.log("useEffect b, value:", b);
    return () => {
      console.log("unmount useEffect b, value:", b);
    };
  }, [b]);

  const handleClick = () => {
    console.log("Clicked!");
    setA(5);
    b >= 5 ? setB(1) : setB(b + 1);
  };

  return (
    <div>
      <p>
        a: {a}, b: {b}
      </p>
      <button onClick={handleClick}>click me</button>
    </div>
  );
};

ReactDOM.render(<MyComponent />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js" integrity="sha256-32Gmw5rBDXyMjg/73FgpukoTZdMrxuYW7tj8adbN8z4=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js" integrity="sha256-bjQ42ac3EN0GqK40pC9gGi/YixvKyZ24qMP/9HiGW7w=" crossorigin="anonymous"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>

尝试将 setState 包裹在检查状态是否需要更改的 if 语句中 - 如果是,请更改它,否则 return () => {}

例如,

useEffect(() => {
    if(a.currentCondition !== a.desiredCondition) {
        setA();
    }
    return cleanup;
}, [b])