使用 React useState() 钩子更新和合并状态对象
Posted
技术标签:
【中文标题】使用 React useState() 钩子更新和合并状态对象【英文标题】:Updating and merging state object using React useState() hook 【发布时间】:2019-08-15 23:11:18 【问题描述】:我发现 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 可以 不会自动合并更新对象。你可以复制这个 通过将函数更新器形式与对象传播相结合的行为 语法:
setState(prevState =>
// Object.assign would also work
return ...prevState, ...updatedValues;
);
据我所知,两者都有效。那么区别是什么呢?哪一个是最佳实践?我应该使用传递函数(选项 2)来访问先前的状态,还是应该使用扩展语法(选项 1)简单地访问当前状态?
【问题讨论】:
您对本地状态和“挂钩”状态感到困惑。它们是不同的。useState
不是管理本地状态的钩子吗?如果我没记错的话,没有所谓的“钩子”状态。
我希望我有这个权利用于您的明确示例: setState(prevState => return ...prevState, propB: false ) 它似乎对我有用! ?
【参考方案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>
【讨论】:
这是文档中该部分的链接:reactjs.org/docs/hooks-reference.html#functional-updates 是的,useState 钩子是异步的,就像 setState 一样,所以在上面的情况下,count 在添加下一个 count 之前不会更新。如果您正在处理来自状态的数据,请始终使用函数式方式。 当我在函数brokenIncrement
之前插入console.log('render')
,然后点击按钮Broken increment
或Increment
,'render'将被打印一次,看起来setCount
函数可以合二为一,所以功能组件渲染一次。但是如果我修改函数brokenIncrement
或increment
,就像function brokenIncrement() Promise.resolve().then(() => setCount(count + 1); setCount(count + 1);
一样,'render' 将被打印两次。那么我们可以像 Promise 一样在异步函数中将setCount
合二为一吗?
有code link【参考方案2】:
如果有人正在搜索 object
的 useState() 挂钩更新通过输入
const [state, setState] = useState( fName: "", lName: "" );
const handleChange = e =>
const name, value = e.target;
setState(prevState => (
...prevState,
[name]: value
));
;
<input
value=state.fName
type="text"
onChange=handleChange
name="fName"
/>
<input
value=state.lName
type="text"
onChange=handleChange
name="lName"
/>
通过 onSubmit 或按钮点击
setState(prevState => (
...prevState,
fName: 'your updated value here'
));
【讨论】:
非常感谢【参考方案3】:最佳做法是使用单独的调用:
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('some@mail.com');
return <input name="username" value=username.value onChange=username.handleChange/>;
【讨论】:
我想我无法将电话分开。我正在构建一个表单组件,它的状态类似于inputs: 'username': dirty: true, touched: true, etc, 'email': dirty: false, touched: false, etc
。我最终会得到大量的状态变量。我真的需要一个带有嵌套对象的状态。
我已经更新了我的答案。您需要使用useReducer
或自定义挂钩。【参考方案4】:
哪一个是使用状态钩子更新状态对象的最佳实践?
正如其他答案所指出的那样,它们都是有效的。
有什么区别?
似乎混淆是由于"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] );
...
它们都在toggle
处理程序中切换propA/B
。
他们都只更新了一个作为e.target.name
传递的道具。
查看仅更新 setMyState
中的一个属性时所产生的差异。
以下演示显示单击propA
会引发错误(仅发生setMyState
),
你可以跟随
警告:组件正在将复选框类型的受控输入更改为不受控制。输入元素不应从受控切换到不受控(反之亦然)。决定在组件的生命周期内使用受控输入元素还是不受控输入元素。
这是因为当您单击propA
复选框时,propB
值被删除,只有propA
值被切换,从而使propB
的checked
值未定义,从而使复选框不受控制。
this.setState
一次只更新一个属性,但它merges
另一个属性,因此复选框保持受控状态。
我通过源代码挖掘,行为是由于useState
调用useReducer
在内部,useState
调用 useReducer
,它返回 reducer 返回的任何状态。
https://github.com/facebook/react/blob/2b93d686e3/packages/react-reconciler/src/ReactFiberHooks.js#L1230
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
);
所以如果你只设置一个属性,其他属性不会合并。
【讨论】:
【参考方案5】:关于状态类型的一个或多个选项可能适合您的用例
通常你可以按照以下规则来决定你想要的那种状态
第一:各个状态是否相关
如果您在应用程序中拥有的各个状态彼此相关,那么您可以选择将它们组合在一个对象中。否则最好将它们分开并使用多个useState
,这样在处理特定处理程序时,您只需要更新相关的状态属性,而不关心其他的
例如,name, email
等用户属性是相关的,您可以将它们组合在一起,而要维护多个计数器,您可以使用 multiple useState hooks
第二:更新状态的逻辑是否复杂,取决于处理程序或用户交互
在上述情况下,最好使用useReducer
进行状态定义。当您尝试创建例如和待办事项应用程序时,这种情况非常常见,您希望在不同的交互中添加 update
、create
和 delete
元素
我是否应该使用传递函数(选项 2)来访问上一个 状态,或者我应该简单地使用扩展语法访问当前状态 (选项 1)?
使用钩子的状态更新也是批处理的,因此,每当您想根据前一个更新状态时,最好使用回调模式。
当 setter 由于只定义一次而没有从封闭的闭包中接收到更新的值时,更新状态的回调模式也会派上用场。例如,如果 useEffect
在添加更新事件状态的侦听器时仅在初始渲染时被调用。
【讨论】:
【参考方案6】:两者都非常适合该用例。传递给setState
的函数参数仅在您想通过区分先前状态来有条件地设置状态时才真正有用(我的意思是您可以使用围绕对setState
的调用的逻辑来做到这一点,但我认为它看起来更干净在函数中),或者如果您在闭包中设置状态,该闭包无法立即访问先前状态的最新版本。
一个例子就像一个事件监听器,在挂载到窗口时只绑定一次(无论出于何种原因)。例如
useEffect(function()
window.addEventListener("click", handleClick)
, [])
function handleClick()
setState(prevState => (...prevState, new: true ))
如果 handleClick
仅使用选项 1 设置状态,则它看起来像 setState(...prevState, new: true )
。但是,这可能会引入一个错误,因为prevState
只会捕获初始渲染时的状态,而不是任何更新。传递给 setState
的函数参数始终可以访问您的状态的最新迭代。
【讨论】:
【参考方案7】:这两个选项都有效,但它们确实有所作为。 使用选项 1 (setCount(count + 1)) 如果
-
属性在更新浏览器时在视觉上并不重要
牺牲刷新率换取性能
根据事件更新输入状态(即event.target.value);如果您使用选项 2,它会由于性能原因将 event 设置为 null,除非您有 event.persist() - 请参阅 event pooling。
使用选项 2 (setCount(c => c + 1)) 如果
-
属性在浏览器上更新时很重要
牺牲性能以获得更好的刷新率
当一些具有自动关闭功能的警报应该分批关闭时,我注意到了这个问题。
注意:我没有统计数据证明性能差异,但它基于关于 React 16 性能优化的 React 会议。
【讨论】:
【参考方案8】:我要提出的解决方案比上面的解决方案简单得多不会搞砸,并且与useState
API具有相同的用法。
使用 npm 包use-merge-state
(here)。将其添加到您的依赖项中,然后像这样使用它:
const useMergeState = require("use-merge-state") // Import
const [state, setState] = useMergeState(initial_state, merge: true) // Declare
setState(new_state) // Just like you set a new state with 'useState'
希望这对每个人都有帮助。 :)
【讨论】:
【参考方案9】:我发现使用useReducer
钩子来管理复杂状态非常方便,而不是useState
。你像这样初始化状态和更新函数:
const initialState = name: "Bob", occupation: "builder" ;
const [state, updateState] = useReducer(
(state, updates) => (
...state,
...updates,
),
initialState
);
然后你就可以通过只传递部分更新来更新你的状态:
updateState( ocupation: "postman" )
【讨论】:
以上是关于使用 React useState() 钩子更新和合并状态对象的主要内容,如果未能解决你的问题,请参考以下文章
使用 React 的 useState 钩子时输入可空状态的正确方法
反应钩子:useState/context;无法读取未定义的属性“头像”/如何更新嵌套对象
如何在 React 中正确使用 useState 钩子和 typescript?