假设我有一个依赖于另一个状态的状态(例如当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>
  )
}

当前回答

▶1。我可以在useEffect钩子中设置状态吗?

原则上,您可以在需要的地方自由设置状态——包括在useEffect内部,甚至在呈现期间。只要确保通过适当地设置Hook deps和/或有条件地设置状态来避免无限循环。


▶2。假设我有一个依赖于另一个状态的状态。创建一个观察a并在useEffect钩子中设置B的钩子是否合适?

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

当您有涉及多个子值的复杂状态逻辑时,或者当下一个状态依赖于前一个状态时,useReducer通常比useState更可取。(反应文档) 当设置一个状态变量取决于另一个状态变量的当前值时,您可能想尝试用useReducer替换它们。 […当你发现自己在写setSomething(something =>…)时,是时候考虑使用减速器了。(丹·阿布拉莫夫,过度反应博客)

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。这样构造代码在性能上有什么缺点吗?

是的。通过将b的状态变化包装在单独的useEffect中,浏览器有了一个额外的布局/绘制阶段——这些效果可能对用户可见。如果你没有办法给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>

其他回答

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

在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

在本例中发生这种情况的原因是,当您更改两个道具时,两个useEffects运行在相同的react周期中。A和道具。B所以{…setState}在两个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钩子中设置状态吗?

原则上,您可以在需要的地方自由设置状态——包括在useEffect内部,甚至在呈现期间。只要确保通过适当地设置Hook deps和/或有条件地设置状态来避免无限循环。


▶2。假设我有一个依赖于另一个状态的状态。创建一个观察a并在useEffect钩子中设置B的钩子是否合适?

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

当您有涉及多个子值的复杂状态逻辑时,或者当下一个状态依赖于前一个状态时,useReducer通常比useState更可取。(反应文档) 当设置一个状态变量取决于另一个状态变量的当前值时,您可能想尝试用useReducer替换它们。 […当你发现自己在写setSomething(something =>…)时,是时候考虑使用减速器了。(丹·阿布拉莫夫,过度反应博客)

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。这样构造代码在性能上有什么缺点吗?

是的。通过将b的状态变化包装在单独的useEffect中,浏览器有了一个额外的布局/绘制阶段——这些效果可能对用户可见。如果你没有办法给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>

效果总是在渲染阶段完成后执行,即使你在一个效果内设置了状态,另一个效果将读取更新的状态并在渲染阶段后对其采取行动。

话虽如此,采取这两种行动可能会产生相同的效果,除非有可能b会因为改变a以外的原因而改变,在这种情况下,你也会想要执行相同的逻辑

useEffect可以钩住某个道具或状态。所以,为了避免无限循环钩子,你需要做的事情是绑定一些变量或状态到效果

例如:

useEffect(myeffectCallback, [])

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

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

上面的效果只会触发我的状态已经改变,这与componentDidUpdate类似,只是不是每个改变的状态都会触发它。

你可以通过这个链接阅读更多细节

一般来说,在useEffect中使用setState将创建一个无限循环,而这很可能是您不想引起的。这个规则有几个例外,我稍后会讲到。

每次渲染后都会调用useEffect,当setState在其中使用时,它会导致组件重新渲染,从而调用useEffect,等等。

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