假设我有一个依赖于其他状态的状态(例如,当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>
)
}
一般来说,在useEffect
中使用setState
将创建一个无限循环,而这很可能是您不想引起的。这个规则有几个例外,我稍后会讨论。
useEffect
在每次渲染后被调用,当setState
在其中使用时,它将导致组件重新渲染,该组件将调用useEffect
,依此类推
在useEffect
内部使用useState
不会导致无限循环的一种常见情况是,当您像useEffect(() => {....}, [])
一样将空数组作为第二个参数传递给useEffect
时,这意味着效果函数应该调用一次:仅在第一次装载/渲染之后。当您在组件中进行数据提取,并且希望将请求数据保存在组件的状态中时,这种方法被广泛使用。
对于未来的目的,这也可能有所帮助:
在useEffect
中使用setState是可以的,您只需要像前面描述的那样注意不要创建循环。
但这并不是可能出现的唯一问题。见下文:
假设您有一个组件Comp
,它从父级接收props
,并根据props
的更改来设置Comp
的状态。出于某种原因,您需要在不同的useEffect
:中为每个道具进行更改
不要这样做
useEffect(() => {
setState({ ...state, a: props.a });
}, [props.a]);
useEffect(() => {
setState({ ...state, b: props.b });
}, [props.b]);
它可能永远不会改变的状态,正如您在本例中看到的:https://codesandbox.io/s/confident-lederberg-dtx7w
在本例中发生这种情况的原因是,当您同时更改prop.a
和prop.b
时,两个useEffects都在同一个反应周期中运行,因此当您更改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
时,您总是会收到最新、最正确的状态值。
效果总是在渲染阶段完成后执行,即使在一个效果内设置state,另一个效果也会读取更新后的状态,并仅在渲染阶段后对其执行操作。
话虽如此,最好采取两种相同效果的操作,除非b
可能由于changing a
以外的原因而发生变化,在这种情况下,您也希望执行相同的逻辑
useEffect
可以挂接某个道具或状态。因此,要避免无限循环挂钩,需要做的事情是绑定一些变量或状态来影响
例如:
useEffect(myeffectCallback, [])
以上效果将仅在组件渲染后激发。这类似于componentDidMount
生命周期
const [something, setSomething] = withState(0)
const [myState, setMyState] = withState(0)
useEffect(() => {
setSomething(0)
}, myState)
以上效果只会触发我的状态发生了变化,这与componentDidUpdate
类似,只是不是每个变化的状态都会触发它。
你可以通过这个链接阅读更多的细节
▶1.我可以在useEffect挂钩内设置状态吗?
原则上,您可以在需要的地方自由设置状态,包括useEffect
内部,甚至在渲染期间。只要确保通过正确设置Hookdeps
和/或有条件地设置状态来避免无限循环。
▶2.假设我有一个依赖于其他状态的状态。在useEffect钩子内创建一个观察a并设置B的钩子合适吗?
您刚刚描述了useReducer
:的经典用例
useReducer
通常比useState
更可取,因为复杂状态逻辑包含多个子值,或者下一个状态依赖于上一个状态。(反应文档)当设置状态变量取决于另一个状态
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
的状态更改包装在a
的单独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>
尝试将setState包装在if语句中,该语句检查状态是否需要更改-如果需要,则更改,否则为return () => {}
例如
useEffect(() => {
if(a.currentCondition !== a.desiredCondition) {
setA();
}
return cleanup;
}, [b])