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

Updating and merging state object using React useState() hook

我发现 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 我们得到:

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

据我所知,两者都有效。那么区别是什么呢?哪一个是最佳实践?我应该使用传递函数(选项 2)来访问以前的状态,还是应该使用扩展语法(选项 1)简单地访问当前状态?

这两个选项都是有效的,但就像 class 组件中的 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('some@mail.com');

  return <input name="username" value={username.value} onChange={username.handleChange}/>;
}

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

举个例子,类似于事件侦听器,在挂载到 window 时只绑定一次(无论出于何种原因)。例如

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

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

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

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

一般您可以按照以下规则来决定您想要的状态类型

第一:各个州是否相关

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

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

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

在上述情况下,最好使用 useReducer 来定义状态。当您尝试在不同的交互

上创建要 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)?

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

更新状态的回调模式在 setter 由于仅定义一次而未从封闭的闭包中接收到更新值时也会派上用场。例如,如果 useEffect 在添加一个更新事件状态的侦听器时仅在初始渲染时被调用。

Which one is the best practice for updating a state object using the state hook?

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

what is the difference?

似乎混淆是由于 "Unlike the setState method found in class components, useState does not automatically merge update objects",尤其是 "merge" 部分。

让我们比较一下 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 发生),

你可以跟随

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.

这是因为当您点击 propA 复选框时,propB 值被删除,只有 propA 值被切换,因此 propBchecked未定义的值使复选框不受控制。

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


我仔细研究了源代码,该行为是由于 useState 调用 useReducer

在内部,useState 调用 useReducer,其中 returns 无论 reducer 处于何种状态 returns。

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 {
        ...
      }
    },

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

如果您熟悉 Redux,您通常会像在选项 1 中那样 return 通过在先前的状态上传播一个新对象。

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

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

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

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

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

  1. 属性 在浏览器上更新时确实很重要
  2. 牺牲性能换取更好的刷新率

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

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

如果有人正在搜索 useState() 钩子更新 object

  • 通过输入

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

我要提出的解决方案比上面的解决方案更简单更容易不会搞砸,并且用法与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'

希望对大家有所帮助。 :)

我发现使用 useReducer 挂钩管理复杂状态非常方便,而不是 useState。你像这样初始化状态和更新函数:

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

然后您可以通过仅传递部分更新来更新您的状态:

updateState({ ocupation: "postman" })