简体   繁体   English

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

[英]Can I set state inside a useEffect hook

Lets say I have some state that is dependent on some other state (eg when A changes I want B to change).假设我有一些状态依赖于其他一些状态(例如,当 A 改变时我希望 B 改变)。

Is it appropriate to create a hook that observes A and sets B inside the useEffect hook?创建一个钩子来观察 A 并在 useEffect 钩子中设置 B 是否合适?

Will the effects cascade such that, when I click the button, the first effect will fire, causing b to change, causing the second effect to fire, before the next render?效果是否会级联,当我单击按钮时,第一个效果会触发,导致 b 发生变化,导致第二个效果在下一次渲染之前触发? Are there any performance downsides to structuring code like this?像这样构造代码有任何性能上的缺点吗?

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>
  )
}

Generally speaking, using setState inside useEffect will create an infinite loop that most likely you don't want to cause.一般来说,在useEffect中使用setState会创建一个您很可能不希望导致的无限循环。 There are a couple of exceptions to that rule which I will get into later.该规则有几个例外情况,我将在稍后讨论。

useEffect is called after each render and when setState is used inside of it, it will cause the component to re-render which will call useEffect and so on and so on. useEffect在每次渲染后调用,当在其中使用setState时,它将导致组件重新渲染,这将调用useEffect等等。

One of the popular cases that using useState inside of useEffect will not cause an infinite loop is when you pass an empty array as a second argument to useEffect like useEffect(() => {....}, []) which means that the effect function should be called once: after the first mount/render only.useEffect中使用useState不会导致无限循环的常见情况之一是,当您将空数组作为第二个参数传递给useEffect ,例如useEffect(() => {....}, [])这意味着效果函数应该被调用一次:仅在第一次安装/渲染之后。 This is used widely when you're doing data fetching in a component and you want to save the request data in the component's state.当您在组件中获取数据并且希望将请求数据保存在组件的状态中时,这会被广泛使用。

For future purposes, this may help too:为了将来的目的,这也可能有帮助:

It's ok to use setState in useEffect you just need to have attention as described already to not create a loop.useEffect中使用 setState 是可以的,您只需要注意已经描述的不要创建循环。

But it's not the only problem that may occur.但这不是唯一可能发生的问题。 See below:见下文:

Imagine that you have a component Comp that receives props from parent and according to a props change you want to set Comp 's state.想象一下,您有一个组件Comp从父级接收props ,并且根据props更改您想要设置Comp的状态。 For some reason, you need to change for each prop in a different useEffect :出于某种原因,您需要在不同的useEffect中为每个道具进行更改:

DO NOT DO THIS不要这样做

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

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

It may never change the state of a as you can see in this example: https://codesandbox.io/s/confident-lederberg-dtx7w它可能永远不会改变 a 的状态,如您在此示例中所见: https ://codesandbox.io/s/confident-lederberg-dtx7w

The reason why this happen in this example it's because both useEffects run in the same react cycle when you change both prop.a and prop.b so the value of {...state} when you do setState are exactly the same in both useEffect because they are in the same context.在这个例子中发生这种情况的原因是因为当您同时更改prop.aprop.b时,两个 useEffects 在同一个反应周期中运行,所以当您执行setState{...state}的值在两个useEffect中完全相同因为他们在同一个上下文中。 When you run the second setState it will replace the first setState .当您运行第二个setState时,它将替换第一个setState

DO THIS INSTEAD这样做

The solution for this problem is basically call setState like this:这个问题的解决方案基本上是像这样调用setState

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

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

Check the solution here: https://codesandbox.io/s/mutable-surf-nynlx在此处查看解决方案: https ://codesandbox.io/s/mutable-surf-nynlx

Now, you always receive the most updated and correct value of the state when you proceed with the setState .现在,当您继续执行setState时,您总是会收到最新和正确的状态值。

Effects are always executed after the render phase is completed even if you setState inside the one effect, another effect will read the updated state and take action on it only after the render phase.效果总是在渲染阶段完成后执行,即使您在一个效果中设置状态,另一个效果将读取更新的状态并仅在渲染阶段后对其执行操作。

Having said that its probably better to take both actions in the same effect unless there is a possibility that b can change due to reasons other than changing a in which case too you would want to execute the same logic话虽如此,最好以相同的效果采取两种操作,除非b可能由于changing a以外的原因而发生更改,在这种情况下,您也希望执行相同的逻辑

useEffect can hook on a certain prop or state. useEffect可以挂钩某个道具或状态。 so, the thing you need to do to avoid infinite loop hook is binding some variable or state to effect所以,为了避免无限循环钩子,你需要做的是绑定一些变量或状态来生效

For Example:例如:

useEffect(myeffectCallback, [])

above effect will fire only once the component has rendered.上面的效果只会在组件渲染后触发。 this is similar to componentDidMount lifecycle这类似于componentDidMount生命周期

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

above effect will fire only my state has changed this is similar to componentDidUpdate except not every changing state will fire it.上面的效果只会触发我的状态已经改变这类似于componentDidUpdate除了不是每个改变的状态都会触发它。

You can read more detail though this link您可以通过此链接阅读更多详细信息

▶ 1. Can I set state inside a useEffect hook? ▶ 1. 我可以在 useEffect 钩子中设置状态吗?

In principle, you can set state freely where you need it - including inside useEffect and even during rendering .原则上,您可以在需要的地方自由设置状态——包括在useEffect内部, 甚至在渲染期间 Just make sure to avoid infinite loops by settting Hook deps properly and/or state conditionally.只需确保通过正确设置 Hook deps和/或有条件地声明来避免无限循环。


▶ 2. Lets say I have some state that is dependent on some other state. ▶ 2. 假设我有一些状态依赖于其他状态。 Is it appropriate to create a hook that observes A and sets B inside the useEffect hook?创建一个钩子来观察 A 并在 useEffect 钩子中设置 B 是否合适?

You just described the classic use case for useReducer :您刚刚描述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.当您有涉及多个子值复杂状态逻辑或下一个状态依赖于前一个状态时, useReducer通常比useState更可取。 ( 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 .当设置状态变量取决于另一个状态变量的当前值时,您可能想尝试用useReducer替换它们。 [...] When you find yourself writing setSomething(something =>...) , it's a good time to consider using a reducer instead. [...] 当您发现自己正在编写setSomething(something =>...)时,是时候考虑改用 reducer 了。 ( 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. Will the effects cascade such that, when I click the button, the first effect will fire, causing b to change, causing the second effect to fire, before the next render? ▶ 3. 效果会级联吗,当我点击按钮时,第一个效果会触发,导致 b 改变,导致第二个效果在下一次渲染之前触发?

useEffect always runs after the render is committed and DOM changes are applied. useEffect始终在提交渲染并应用 DOM 更改运行。 The first effect fires, changes b and causes a re-render.第一个效果触发,改变b并导致重新渲染。 After this render has completed, second effect will run due to b changes.此渲染完成后,由于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. Are there any performance downsides to structuring code like this? ▶ 4. 像这样构造代码是否有性能上的缺点?

Yes.是的。 By wrapping the state change of b in a separate useEffect for a , the browser has an additional layout/paint phase - these effects are potentially visible for the user.通过将b的状态变化包装在a的单独useEffect中,浏览器有一个额外的布局/绘制阶段——这些效果可能对用户可见。 If there is no way you want give useReducer a try, you could change b state together with a directly:如果你没有办法尝试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>

Try wrapping the setState inside an if-statement that checks whether the state needs to be changed - if yes, change it, else return () => {}尝试将setState包装在检查状态是否需要更改的 if 语句中 - 如果是,请更改它,否则return () => {}

eg,例如,

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM