我发现这两个React Hooks文档有点令人困惑。哪一个是使用状态挂钩更新状态对象的最佳实践?
想象一下想要进行以下状态更新:
INITIAL_STATE = {
propA: true,
propB: true
}
stateAfter = {
propA: true,
propB: false // Changing this property
}
选项1
从Using the React Hook文章中,我们知道这是可能的:
const [count, setCount] = useState(0);
setCount(count + 1);
所以我能做到:
const [myState, setMyState] = useState(INITIAL_STATE);
然后:
setMyState({
...myState,
propB: false
});
方案2
从Hooks Reference我们得到:
与类组件中的setState方法不同,useState不会自动合并更新对象。您可以通过将函数updater表单与对象扩展语法组合来复制此行为:
setState(prevState => {
// Object.assign would also work
return {...prevState, ...updatedValues};
});
据我所知,两者都有效。那么区别是什么呢?哪一个是最佳做法?我应该使用传递函数(选项2)来访问先前的状态,还是应该使用扩展语法(OPTION 1)简单地访问当前状态?
这两个选项都是有效的,但就像在类组件中使用setState
一样,在更新从已经处于状态的状态派生的状态时需要小心。
如果你是连续两次更新计数,如果不使用更新状态的函数版本,它将无法按预期工作。
const { useState } = React;
function App() {
const [count, setCount] = useState(0);
function brokenIncrement() {
setCount(count + 1);
setCount(count + 1);
}
function increment() {
setCount(count => count + 1);
setCount(count => count + 1);
}
return (
<div>
<div>{count}</div>
<button onClick={brokenIncrement}>Broken increment</button>
<button onClick={increment}>Increment</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
最佳做法是使用单独的调用:
const [a, setA] = useState(true);
const [b, setB] = useState(true);
选项1可能会导致更多错误,因为这样的代码通常最终会在一个过时值myState
的闭包内。
当新状态基于旧状态时,应使用选项2:
setCount(count => count + 1);
对于复杂的状态结构,请考虑使用useReducer
对于共享某些形状和逻辑的复杂结构,您可以创建自定义钩子:
function useField(defaultValue) {
const [value, setValue] = useState(defaultValue);
const [dirty, setDirty] = useState(false);
const [touched, setTouched] = useState(false);
function handleChange(e) {
setValue(e.target.value);
setTouched(true);
}
return {
value, setValue,
dirty, setDirty,
touched, setTouched,
handleChange
}
}
function MyComponent() {
const username = useField('some username');
const email = useField('[email protected]');
return <input name="username" value={username.value} onChange={username.handleChange}/>;
}
根据您的使用情况,有关状态类型的一个或多个选项可能是合适的
通常,您可以按照以下规则来确定所需的状态类型
第一:各州是否相关
如果应用程序中的个别状态与另一个状态相关,则可以选择将它们组合在一个对象中。否则最好将它们分开并使用多个useState
,以便在处理特定处理程序时,您只更新相关状态属性而不关心其他属性
例如,name, email
等用户属性是相关的,您可以将它们组合在一起而不是维护多个计数器,您可以使用multiple useState hooks
第二:更新状态的逻辑是否复杂并且取决于处理程序或用户交互
在上面的例子中,最好使用useReducer
进行状态定义。当您尝试创建示例和todo应用程序时,您希望在不同的交互中使用update
,create
和delete
元素,这种情况非常常见
我应该使用传递函数(选项2)来访问先前的状态,还是应该使用扩展语法(OPTION 1)简单地访问当前状态?
使用钩子的状态更新也是批处理的,因此每当你想根据前一个更新状态时,最好使用回调模式。
当setter没有从封闭闭包接收更新值时,更新状态的回调模式也会派上用场,因为它只被定义一次。例如,如果仅在初始渲染时调用useEffect
,则会添加一个更新事件状态的侦听器。
哪一个是使用状态挂钩更新状态对象的最佳实践?
它们都是有效的,正如其他答案所指出的那样。
有什么不同?
似乎混淆是由于"Unlike the setState method found in class components, useState does not automatically merge update objects"
,尤其是“合并”部分。
让我们来比较this.setState
和useState
class SetStateApp extends React.Component {
state = {
propA: true,
propB: true
};
toggle = e => {
const { name } = e.target;
this.setState(
prevState => ({
[name]: !prevState[name]
}),
() => console.log(`this.state`, this.state)
);
};
...
}
function HooksApp() {
const INITIAL_STATE = { propA: true, propB: true };
const [myState, setMyState] = React.useState(INITIAL_STATE);
const { propA, propB } = myState;
function toggle(e) {
const { name } = e.target;
setMyState({ [name]: !myState[name] });
}
...
}
他们两个都在propA/B
处理程序中切换toggle
。他们都只更新了一个作为e.target.name
传递的道具。
查看在setMyState
中仅更新一个属性时所产生的差异。
以下演示显示单击propA
会抛出错误(仅发生setMyState
),
你可以跟随
警告:组件正在更改类型复选框的受控输入以使其不受控制。输入元素不应从受控切换到不受控制(反之亦然)。决定在组件的使用寿命期间使用受控或不受控制的输入元件。
这是因为当你点击propA
复选框时,propB
值被删除,只有propA
值被切换,从而使propB
的checked
值为undefined,使得复选框不受控制。
而this.setState
一次只更新一个属性,但它merges
其他属性因此复选框保持控制。
我挖了源代码,行为是由于useState
调用useReducer
在内部,useState
调用useReducer
,它返回reducer返回的任何状态。
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
...
try {
return updateState(initialState);
} finally {
...
}
},
其中updateState
是useReducer
的内部实现。
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
currentHookNameInDev = 'useReducer';
updateHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateReducer(reducer, initialArg, init);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
如果您熟悉Redux,通常会像在选项1中那样通过扩展先前的状态来返回新对象。
setMyState({
...myState,
propB: false
});
因此,如果只设置一个属性,则不会合并其他属性。
这两个选项都有效但它们确实有所作为。使用选项1(setCount(count + 1))if
如果使用选项2(setCount(c => c + 1))
当一些具有autoclose功能的警报应该按批次顺序关闭时,我注意到了这个问题。
注意:我没有证明性能差异的统计数据,但它基于React 16性能优化的React会议。
两者都非常适合该用例。传递给setState
的函数参数只有当你想通过区分前一个状态来有条件地设置状态时才真正有用(我的意思是你可以用围绕调用setState
的逻辑来做它但我认为它在函数中看起来更干净)或者如果您在闭包中设置状态,该闭包不能立即访问以前状态的最新版本。
一个例子就像一个事件监听器,它只能在挂载到窗口时绑定一次(无论出于何种原因)。例如。
useEffect(function() {
window.addEventListener("click", handleClick)
}, [])
function handleClick() {
setState(prevState => ({...prevState, new: true }))
}
如果handleClick
仅使用选项1设置状态,它看起来像setState({...prevState, new: true })
。但是,这可能会引入一个错误,因为prevState
只捕获初始渲染时的状态,而不是任何更新。传递给setState
的函数参数始终可以访问您所在州的最新迭代。