在 useEffect 钩子函数中可以设置状态吗?

278

假设我有一些状态取决于其他状态(例如,当A更改时,我希望B也更改)。

创建一个钩子来观察A并在useEffect钩子内设置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>
  )
}
6个回答

232

一般来说,在useEffect中使用setState会导致无限循环,这很可能不是你想要的结果。但有几种例外情况,稍后将详细介绍。

useEffect在每次渲染后都会被调用,当在其中使用setState时,它会导致组件重新渲染,进而调用useEffect,如此往复。

useEffect中使用useState而不会导致无限循环的一个常见场景是,在第二个参数传递一个空数组[],例如:useEffect(() => {....}, [])。这意味着该effect函数应该只被调用一次:在首次挂载/渲染之后。在组件进行数据获取并且希望将请求数据保存在组件状态中时,这种做法经常被广泛使用。


5
useEffect内部使用setState的另一个情况是在订阅或事件监听器中设置状态。但不要忘记取消订阅。https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup - timqian
89
这并非完全正确 - useState只有在你更新状态的值与上一个不同时才会触发,因此除非在循环周期之间更改该值,否则可以防止出现无限循环。 - RobM
34
答案不正确且与问题无关:在代码中,当按钮被点击时,组件仅渲染两次,不存在无限循环。 - Bogdan D
39
在 useEffect 中设置 state 是非常常见的用例。比如,数据加载时,useEffect 调用 API、获取数据,并使用 useState 的 set 部分进行设置。 - badbod99
6
这个回答的第一段无条件地表明不应该这样做,但这种情况经常发生。也许可以重新排列一下,使你在第三段中添加的条件更加突出。 - nil
显示剩余6条评论

169

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

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

但这不是唯一可能发生的问题。请见下文:

假设您有一个组件Comp从父组件接收props,并根据props更改设置Comp的状态。由于某种原因,您需要为每个prop在不同的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

这种情况发生的原因是因为两个 useEffect 在同一个 react 生命周期中运行,当您同时更改 prop.aprop.b 时,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时,始终会收到最新和正确的状态值。


6
上面的解决方案帮助我使用 setName(name => ({ ...name, a: props.a })); - mufaddal_mw
6
这也帮助了我,在箭头函数setItems(items => [...items, item]) 的部分。 - Stefan Zhelyazkov
2
从早上7点到下午3点都没有找到解决方案,现在你救了我。 - Dinindu Kanchana
以上解决方案可行。直接尝试更改状态并没有帮助。然而,传递一个更新器函数就可以解决问题了。参考:https://reactjs.org/docs/faq-state.html - dodobird
1
可能应该是一个代码检查规则。 - Dan Mandel
显示剩余3条评论

43

即使您在一个effect中setState,另一个effect也会在渲染阶段完成后执行操作,并读取更新后的状态并对其采取行动。

话虽如此,最好在同一个effect中执行两个操作,除非有可能由于除更改a之外的其他原因而导致b发生更改,在这种情况下,您仍希望执行相同的逻辑。


9
如果 A 改变了 B,那么该组件会被渲染两次,对吗? - Dan Ruswick
2
@alaboudi 是的,如果 A 发生变化导致 useeffect 运行并设置 B,则组件会渲染两次。 - Shubham Khatri
@alaboudi 是的,就像 Shubham Khatri 所说的那样,它会重新渲染。 但是你可以使用第二个参数跳过在重新渲染后调用你的 effect,请参考 https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects。 - Karthikeyan

38

useEffect可以挂在特定的属性或状态上。因此,为了避免无限循环挂钩,您需要将某些变量或状态绑定到效果。

例如:

useEffect(myeffectCallback, [])

只有组件渲染完之后,上述效果才会触发。这类似于 componentDidMount 生命周期。


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

以上效果只会在我的状态发生改变时触发,这类似于componentDidUpdate,但并不是每次更改状态都会触发它。

您可以通过此链接了解更多详细信息。


2
谢谢,这个答案解决了useEffect的依赖数组问题,而其他答案没有。将空数组作为useEffect的第二个参数包含进去,可以确保useEffect在组件渲染后执行一次,但是如果包含一个特定状态或多个特定状态的数组,useEffect会在引用状态发生变化时执行。 - adriaanbd
10
我不明白 withState() 代表什么意思,在文档中也找不到相关的参考资料。 - Mike S.

30

▶ 1. 我可以在useEffect hook里面设置状态吗?

原则上,您可以在需要的任何地方自由设置状态 - 包括在 useEffect 中和甚至是在渲染期间。只需确保通过适当设置 Hook 的依赖项和/或有条件地设置状态来避免无限循环。


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

您刚刚描述了 the 经典用例,也就是使用 useReducer 的情况:

当您有涉及多个子值或下一个状态取决于前一个状态的复杂状态逻辑时,通常更喜欢使用 useReducer 而不是 useState。(React文档

当设置一个 状态变量 取决于另一个 状态变量 的当前值时,您可能希望尝试将它们都替换为 useReducer。 [...] 当您发现自己在编写 setSomething(something => ...) 时,是考虑使用reducer的好时机。 (Dan Abramov, Overreacted博客

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 总是在渲染提交和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. 这种代码结构有什么性能上的缺陷吗?

有的。通过在单独的 useEffect 中包装对 b 的状态更改,浏览器会有一个额外的布局/绘制阶段 - 这些效果对用户可能是可见的。如果你不想尝试 useReducer,你可以直接与 a 一起更改 b 状态:


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>


3
尝试将setState语句包含在if语句中,检查状态是否需要更改,如果是,则将其更改,否则return () => {}
例如:
useEffect(() => {
    if(a.currentCondition !== a.desiredCondition) {
        setA();
    }
    return cleanup;
}, [b])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接