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

IT技术 javascript reactjs react-hooks
2021-01-16 13:52:32

我发现 React Hooks 文档的这两部分有点令人困惑。哪个是使用状态挂钩更新状态对象的最佳实践?

想象一下想要进行以下状态更新:

INITIAL_STATE = {
  propA: true,
  propB: true
}

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

选项1

使用 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)简单地访问当前状态?

6个回答

这两个选项都是有效的,但就像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>

当我console.log('render')在函数之前插入brokenIncrement,然后点击按钮Broken increment或者Increment,'render'会打印一次,好像setCount函数可以合二为一,所以函数组件渲染一次。但是,如果我修改函数brokenIncrementor increment ,就像function brokenIncrement() { Promise.resolve().then(() => {setCount(count + 1); setCount(count + 1);}}'render' 将打印两次。那么我们可以setCount在 async 函数中合二为一,就像 Promise 一样。
2021-03-25 13:52:32
是的,useState 钩子是异步的,就像 setState 一样,所以在上面的情况下,在添加下一个计数之前不会更新计数。如果您正在处理来自状态的数据,请始终使用函数式方式。
2021-04-06 13:52:32
2021-04-07 13:52:32
2021-04-10 13:52:32

如果有人正在搜索 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'
        }));
    
非常感谢你
2021-03-26 13:52:32

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

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}我最终会得到大量的状态变量。我真的需要一个带有嵌套对象的状态。
2021-03-21 13:52:32
我已经更新了我的答案。您需要使用useReducer或自定义挂钩。
2021-03-31 13:52:32

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

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

有什么区别?

似乎混乱是由于"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/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 {
        ...
      }
    },

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

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

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

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

第一:各个州是否相关

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

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

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

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

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

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

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