简体   繁体   English

使用 React useState() 钩子更新和合并状态对象

[英]Updating and merging state object using React useState() hook

I'm finding these two pieces of the React Hooks docs a little confusing.我发现 React Hooks 文档的这两部分有点令人困惑。 Which one is the best practice for updating a state object using the state hook?哪个是使用状态挂钩更新状态对象的最佳实践?

Imagine a want to make the following state update:想象一下想要进行以下状态更新:

INITIAL_STATE = {
  propA: true,
  propB: true
}

stateAfter = {
  propA: true,
  propB: false   // Changing this property
}

OPTION 1选项1

From the Using the React Hook article, we get that this is possible:使用 React Hook文章中,我们了解到这是可能的:

const [count, setCount] = useState(0);
setCount(count + 1);

So I could do:所以我可以这样做:

const [myState, setMyState] = useState(INITIAL_STATE);

And then:接着:

setMyState({
  ...myState,
  propB: false
});

OPTION 2选项 2

And from the Hooks Reference we get that:Hooks Reference 中我们得到:

Unlike the setState method found in class components, useState does not automatically merge update objects.与类组件中的 setState 方法不同,useState 不会自动合并更新对象。 You can replicate this behavior by combining the function updater form with object spread syntax:您可以通过将函数更新程序形式与对象传播语法相结合来复制此行为:

setState(prevState => {
  // Object.assign would also work
  return {...prevState, ...updatedValues};
});

As far as I know, both works.据我所知,两者都有效。 So, what is the difference?那么区别是什么呢? Which one is the best practice?哪一个是最佳实践? Should I use pass the function (OPTION 2) to access the previous state, or should I simply access the current state with spread syntax (OPTION 1)?我应该使用传递函数(选项 2)来访问以前的状态,还是应该使用扩展语法(选项 1)简单地访问当前状态?

Both options are valid, but just as with setState in a class component you need to be careful when updating state derived from something that already is in state.这两个选项都是有效的,但就像类组件中的setState ,在更新从已经处于状态的事物派生的状态时需要小心。

If you eg update a count twice in a row, it will not work as expected if you don't use the function version of updating the state.例如,如果您连续两次更新计数,如果您不使用更新状态的功能版本,它将无法按预期工作。

 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>

The best practice is to use separate calls:最佳做法是使用单独的调用:

const [a, setA] = useState(true);
const [b, setB] = useState(true);

Option 1 might lead to more bugs because such code often end up inside a closure which has an outdated value of myState .选项 1 可能会导致更多错误,因为此类代码通常最终位于具有过时值myState的闭包中。

Option 2 should be used when the new state is based on the old one:当新状态基于旧状态时,应使用选项 2:

setCount(count => count + 1);

For complex state structure consider using useReducer对于复杂的状态结构,请考虑使用useReducer

For complex structures that share some shape and logic you can create a custom hook:对于共享某些形状和逻辑的复杂结构,您可以创建自定义钩子:

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}/>;
}

If anyone is searching for useState() hooks update for object如果有人正在搜索useState()挂钩更新对象

- Through Input

        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"
        />
   ***************************

 - Through onSubmit or button click

        setState(prevState => ({
            ...prevState,
            fName: 'your updated value here'
         }));

Which one is the best practice for updating a state object using the state hook?哪个是使用状态挂钩更新状态对象的最佳实践?

They are both valid as other answers have pointed out.正如其他答案所指出的那样,它们都是有效的。

what is the difference?有什么区别?

It seems like the confusion is due to "Unlike the setState method found in class components, useState does not automatically merge update objects" , especially the "merge" part.似乎混淆是由于"Unlike the setState method found in class components, useState does not automatically merge update objects" ,尤其是“合并”部分。

Let's compare this.setState & useState让我们比较一下this.setStateuseState

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] });
  }
...
}

Both of them toggles propA/B in toggle handler.它们都在toggle处理程序中切换propA/B And they both update just one prop passed as e.target.name .他们e.target.name更新了一个作为e.target.name传递的道具。

Check out the difference it makes when you update just one property in setMyState .看看当你只更新setMyState一个属性时它会产生什么不同。

Following demo shows that clicking on propA throws an error(which occurs setMyState only),以下演示显示单击propA会引发错误(仅发生setMyState ),

You can following along你可以跟着

编辑 nrrjqj30wp

Warning: A component is changing a controlled input of type checkbox to be uncontrolled.警告:组件正在将复选框类型的受控输入更改为不受控制。 Input elements should not switch from controlled to uncontrolled (or vice versa).输入元素不应从受控切换到不受控制(反之亦然)。 Decide between using a controlled or uncontrolled input element for the lifetime of the component.决定在组件的生命周期内使用受控或非受控输入元素。

错误演示

It's because when you click on propA checkbox, propB value is dropped and only propA value is toggled thus making propB 's checked value as undefined making the checkbox uncontrolled.这是因为当您点击propA复选框时, propB值被删除,只有propA值被切换,从而使propBchecked值未定义,使复选框不受控制。

And the this.setState updates only one property at a time but it merges other property thus the checkboxes stay controlled.而且this.setState只更新一个属性,但它merges其他属性,因此复选框保持受控。


I dug thru the source code and the behavior is due to useState calling useReducer我挖掘了源代码,行为是由于useState调用useReducer

Internally, useState calls useReducer , which returns whatever state a reducer returns.在内部, useState调用useReducer ,它返回 reducer 返回的任何状态。

https://github.com/facebook/react/blob/2b93d686e3/packages/react-reconciler/src/ReactFiberHooks.js#L1230 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 {
        ...
      }
    },

where updateState is the internal implementation for useReducer .其中updateStateuseReducer的内部实现。

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;
      }
    },

If you are familiar with Redux, you normally return a new object by spreading over previous state as you did in option 1.如果您熟悉 Redux,您通常会像在选项 1 中那样通过扩展先前状态来返回一个新对象。

setMyState({
  ...myState,
  propB: false
});

So if you set just one property, other properties are not merged.因此,如果您只设置一个属性,则不会合并其他属性。

One or more options regarding state type can be suitable depending on your usecase根据您的用例,一个或多个关于状态类型的选项可能适用

Generally you could follow the following rules to decide the sort of state that you want通常,您可以遵循以下规则来决定您想要的状态类型

First: Are the individual states related第一:各个州是否相关

If the individual state that you have in your application are related to one other then you can choose to group them together in an object.如果您的应用程序中的各个状态彼此相关,那么您可以选择将它们组合在一个对象中。 Else its better to keep them separate and use multiple useState so that when dealing with specific handlers you are only updating the relavant state property and are not concerned about the others否则最好将它们分开并使用多个useState以便在处理特定处理程序时您只更新相关状态属性而不关心其他

For instance, user properties such as name, email are related and you can group them together Whereas for maintaining multiple counters you can make use of multiple useState hooks例如, name, email等用户属性是相关的,您可以将它们组合在一起,而为了维护多个计数器,您可以使用multiple useState hooks

Second: Is the logic to update state complex and depends on the handler or user interaction第二:更新状态的逻辑是否复杂,取决于处理程序或用户交互

In the above case its better to make use of useReducer for state definition.在上述情况下,最好使用useReducer进行状态定义。 Such kind of scenario is very common when you are trying to create for example and todo app where you want to update , create and delete elements on different interactions当您尝试创建例如要在不同交互中updatecreatedelete元素的待办事项应用程序时,这种情况非常常见。

Should I use pass the function (OPTION 2) to access the previous state, or should I simply access the current state with spread syntax (OPTION 1)?我应该使用传递函数(选项 2)来访问以前的状态,还是应该使用扩展语法(选项 1)简单地访问当前状态?

state updates using hooks are also batched and hence whenever you want to update state based on previous one its better to use the callback pattern.使用钩子的状态更新也是批处理的,因此每当你想根据前一个更新状态时,最好使用回调模式。

The callback pattern to update state also comes in handy when the setter doesn't receive updated value from enclosed closure due to it being defined only once.当 setter 没有从封闭的闭包中接收到更新的值时,更新状态的回调模式也会派上用场,因为它只被定义了一次。 An example of such as case if the useEffect being called only on initial render when adds a listener that updates state on an event.例如,当添加更新事件状态的侦听器时,仅在初始渲染时调用useEffect情况的示例。

Both are perfectly fine for that use case.两者都非常适合该用例。 The functional argument that you pass to setState is only really useful when you want to conditionally set the state by diffing the previous state (I mean you can just do it with logic surrounding the call to setState but I think it looks cleaner in the function) or if you set state in a closure that doesn't have immediate access to the freshest version of previous state.您传递给setState的函数参数仅在您想通过比较前一个状态来有条件地设置状态时才真正有用(我的意思是您可以使用围绕setState调用的逻辑来完成,但我认为它在函数中看起来更清晰)或者如果您在无法立即访问先前状态的最新版本的闭包中设置状态。

An example being something like an event listener that is only bound once (for whatever reason) on mount to the window.一个例子是一个事件侦听器,它在安装到窗口时只绑定一次(无论出于何种原因)。 Eg例如

useEffect(function() {
  window.addEventListener("click", handleClick)
}, [])

function handleClick() {
  setState(prevState => ({...prevState, new: true }))
}

If handleClick was only setting the state using option 1, it would look like setState({...prevState, new: true }) .如果handleClick仅使用选项 1 设置状态,则它看起来像setState({...prevState, new: true }) However, this would likely introduce a bug because prevState would only capture the state on initial render and not from any updates.但是,这可能会引入一个错误,因为prevState只会在初始渲染时捕获状态,而不会从任何更新中捕获状态。 The function argument passed to setState would always have access to the most recent iteration of your state.传递给setState的函数参数将始终可以访问状态的最新迭代。

Both options are valid but they do make a difference.这两个选项都是有效的,但它们确实有所不同。 Use Option 1 (setCount(count + 1)) if使用选项 1 (setCount(count + 1)) 如果

  1. Property doesn't matter visually when it updates browser属性在更新浏览器时在视觉上无关紧要
  2. Sacrifice refresh rate for performance牺牲刷新率换取性能
  3. Updating input state based on event (ie event.target.value);根据事件更新输入状态(即 event.target.value); if you use Option 2, it will set event to null due to performance reasons unless you have event.persist() - Refer to event pooling .如果您使用选项 2,它将由于性能原因将 event 设置为 null,除非您有 event.persist() - 请参阅事件池

Use Option 2 (setCount(c => c + 1)) if使用选项 2 (setCount(c => c + 1)) 如果

  1. Property does matter when it updates on the browser属性在浏览器上更新时很重要
  2. Sacrifice performance for better refresh rate牺牲性能以获得更好的刷新率

I noticed this issue when some Alerts with autoclose feature that should close sequentially closed in batches.当一些具有自动关闭功能的警报应该按顺序关闭时,我注意到了这个问题。

Note: I don't have stats proving the difference in performance but its based on a React conference on React 16 performance optimizations.注意:我没有证明性能差异的统计数据,但它基于关于 React 16 性能优化的 React 会议。

The solution I am going to propose is much simpler and easier to not mess up than the ones above, and has the same usage as the useState API .我要提出的解决方案比上面的解决方案更简单、更容易避免混乱,并且useState API具有相同的用法

Use the npm package use-merge-state ( here ) .使用 npm 包use-merge-state此处 Add it to your dependencies, then, use it like:将其添加到您的依赖项中,然后像这样使用它:

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'

Hope this helps everyone.希望这对大家有帮助。 :) :)

I find it very convenient to use useReducer hook for managing complex state, instead od useState .我发现使用useReducer hook 来管理复杂状态非常方便,而不是 od useState You initialize state and updating function like this:您可以像这样初始化状态和更新函数:

const initialState = { name: "Bob", occupation: "builder" };
const [state, updateState] = useReducer(
  (state, updates) => ({
    ...state,
    ...updates,
  }),
  initialState
);

And then you're able to update your state by only passing partial updates:然后你就可以通过只传递部分更新来更新你的状态:

updateState({ ocupation: "postman" })

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

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