Updating and merging state object using React useState() hook

cbd*_*per 24 javascript reactjs react-hooks

I'm finding these two pieces of the React Hooks docs a little confusing. 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
}
Run Code Online (Sandbox Code Playgroud)

OPTION 1

From the Using the React Hook article, we get that this is possible:

const [count, setCount] = useState(0);
setCount(count + 1);
Run Code Online (Sandbox Code Playgroud)

So I could do:

const [myState, setMyState] = useState(INITIAL_STATE);
Run Code Online (Sandbox Code Playgroud)

And then:

setMyState({
  ...myState,
  propB: false
});
Run Code Online (Sandbox Code Playgroud)

OPTION 2

And from the Hooks Reference we get that:

Unlike the setState method found in class components, useState does not automatically merge update objects. 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};
});
Run Code Online (Sandbox Code Playgroud)

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)?

Tho*_*lle 25

这两个选项都是有效的,但是就像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"));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

  • 这是文档中该部分的链接:https://reactjs.org/docs/hooks-reference.html#functional-updates (4认同)

Mah*_*rus 18

如果有人正在搜索 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'
         }));
Run Code Online (Sandbox Code Playgroud)

  • 非常感谢你 (2认同)
  • 这就是你想要的 2022 年和现代 React 的答案。非常感谢,由于某种原因,这个非常基本的想法一直困扰着我。很棒的例子。 (2认同)

dan*_*die 8

哪个是使用状态挂钩更新状态对象的最佳实践?

正如其他答案所指出的那样,它们都是有效的。

有什么不同?

似乎混乱是由于"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] });
  }
...
}

Run Code Online (Sandbox Code Playgroud)

它们都propA/Btoggle处理程序中切换。他们都只更新了一个作为e.target.name.

看看当您只更新setMyState.

以下演示显示单击propA会引发错误(仅发生setMyState),

你可以跟着

编辑 nrrjqj30wp

警告:组件正在将复选框类型的受控输入更改为不受控制。输入元素不应从受控切换到不受控制(反之亦然)。决定在组件的生命周期内使用受控或非受控输入元素。

错误演示

这是因为当您单击propA复选框时,propB值会被删除,并且只有propA值会被切换,从而使propBchecked值未定义,从而使复选框不受控制。

并且this.setState一次只更新一个属性,但更新merges其他属性,因此复选框保持受控。


我挖掘了源代码,行为是由于useState调用useReducer

在内部,useState调用useReducer,它返回减速器返回的任何状态。

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 {
        ...
      }
    },
Run Code Online (Sandbox Code Playgroud)

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;
      }
    },
Run Code Online (Sandbox Code Playgroud)

如果您熟悉 Redux,您通常会像在选项 1 中那样通过扩展先前状态来返回一个新对象。

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

Run Code Online (Sandbox Code Playgroud)

因此,如果您只设置一个属性,则不会合并其他属性。


Uji*_*T34 7

最佳做法是使用单独的调用:

const [a, setA] = useState(true);
const [b, setB] = useState(true);
Run Code Online (Sandbox Code Playgroud)

选项1可能会导致更多错误,因为这样的代码通常最终出现在闭包中,而闭包的值为myState

当新状态基于旧状态时,应使用选项2:

setCount(count => count + 1);
Run Code Online (Sandbox Code Playgroud)

对于复杂的状态结构,请考虑使用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}/>;
}
Run Code Online (Sandbox Code Playgroud)


Shu*_*tri 6

根据您的用例,一个或多个关于状态类型的选项可能适用

通常,您可以遵循以下规则来决定您想要的状态类型

第一:各个州是否相关

如果您在应用程序中拥有的各个状态彼此相关,那么您可以选择将它们组合在一个对象中。否则最好将它们分开并使用多个,useState以便在处理特定处理程序时您只更新相关状态属性而不关心其他处理程序

例如,诸如name, email相关的用户属性,您可以将它们组合在一起,而为了维护多个计数器,您可以使用multiple useState hooks

第二:更新状态的逻辑是否复杂,取决于处理程序或用户交互

在上述情况下,最好使用useReducer状态定义。这样的那种情景是,当你试图创建例如与待办事项应用要非常普遍updatecreate而且delete在不同的互动元素

我应该使用传递函数(选项 2)来访问以前的状态,还是应该使用扩展语法(选项 1)简单地访问当前状态?

使用钩子的状态更新也是批处理的,因此每当你想根据前一个更新状态时,最好使用回调模式。

当 setter 没有从封闭的闭包中接收到更新的值时,更新状态的回调模式也会派上用场,因为它只被定义了一次。例如,useEffect当添加更新事件状态的侦听器时,仅在初始渲染时调用此情况的示例。


Tom*_*ney 5

两者都非常适合该用例。您传递给的函数参数setState仅在您想通过比较前一个状态来有条件地设置状态时才真正有用(我的意思是您可以使用围绕调用的逻辑来完成它,setState但我认为它在函数中看起来更清晰)或者如果您在无法立即访问先前状态的最新版本的闭包中设置状态。

一个例子是一个事件侦听器,它在安装到窗口时只绑定一次(无论出于何种原因)。例如

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

function handleClick() {
  setState(prevState => ({...prevState, new: true }))
}
Run Code Online (Sandbox Code Playgroud)

如果handleClick仅使用选项 1 设置状态,则它看起来像setState({...prevState, new: true }). 但是,这可能会引入一个错误,因为它prevState只会在初始渲染时捕获状态,而不是从任何更新中捕获状态。传递给的函数参数setState将始终可以访问状态的最新迭代。


SIL*_*ENT 5

这两个选项都是有效的,但它们确实有所不同。使用选项 1 (setCount(count + 1)) 如果

  1. 属性在更新浏览器时在视觉上无关紧要
  2. 牺牲刷新率换取性能
  3. 根据事件更新输入状态(即 event.target.value);如果您使用选项 2,它将由于性能原因将 event 设置为 null,除非您有 event.persist() - 请参阅事件池

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

  1. 属性在浏览器上更新时很重要
  2. 牺牲性能以获得更好的刷新率

当一些具有自动关闭功能的警报应该按顺序关闭时,我注意到了这个问题。

注意:我没有证明性能差异的统计数据,但它基于关于 React 16 性能优化的 React 会议。