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

时间:2019-03-25 16:30:41

标签: javascript reactjs react-hooks

我发现这两个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)?

8 个答案:

答案 0 :(得分:8)

这两个选项都是有效的,但是就像在类组件中使用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>

答案 1 :(得分:5)

如果有人在搜索 useState(),则挂钩 object

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

答案 2 :(得分: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}/>;
}

答案 3 :(得分:3)

有关状态类型的一个或多个选项可能适用,具体取决于您的用例

通常,您可以遵循以下规则来确定所需的状态

首先:各个州是否相关

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

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

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

在上述情况下,最好使用useReducer进行状态定义。当您尝试创建例如todo应用程序并希望在不同交互中updatecreatedelete元素的情况下,这种情况非常普遍。

  

我应该使用传递功能(选项2)来访问上一个   状态,还是我应该简单地使用扩展语法访问当前状态   (选项1)?

使用挂钩的

状态更新也会被批量处理,因此,每当您要基于前一个状态更新状态时,最好使用回调模式。

当设置器由于仅定义一次而没有从封闭的闭包中接收到更新的值时,用于更新状态的回调模式也将派上用场。例如,在添加更新事件状态的侦听器时仅在初始渲染上调用useEffect的情况。

答案 4 :(得分:1)

  

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

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

  

有什么区别?

似乎造成混淆的原因是"Unlike the setState method found in class components, useState does not automatically merge update objects",尤其是“合并”部分。

让我们比较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] });
  }
...
}

它们两者都在propA/B处理程序中切换toggle。 而且他们都只更新了一个通过e.target.name传递的道具。

检查一下您仅更新setMyState中的一个属性时的区别。

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

您可以关注

Edit nrrjqj30wp

  

警告:组件正在将类型为复选框的受控输入更改为不受控制。输入元素不应从受控状态切换到非受控状态(反之亦然)。在组件的使用寿命中决定使用受控还是不受控制的输入元素。

error demo

这是因为当您单击propA复选框时,propB的值将被删除,并且仅切换propA的值,从而使propB的{​​{1}}的值成为undefined,使复选框不受控制。

checked一次仅更新一个属性,而this.setState则更新另一个属性,因此复选框保持受控状态。


我通过源代码进行了挖掘,其行为是由于merges调用了useState

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

https://github.com/facebook/react/blob/2b93d686e3/packages/react-reconciler/src/ReactFiberHooks.js#L1230

useReducer

其中 useState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] { currentHookNameInDev = 'useState'; ... try { return updateState(initialState); } finally { ... } }, updateState的内部实现。

useReducer

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

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

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

答案 5 :(得分:1)

两个选项均有效,但确实有所不同。 如果

使用选项1(setCount(count + 1))
  1. 属性在更新浏览器时并不重要
  2. 牺牲性能刷新率
  3. 基于事件更新输入状态(即event.target.value);如果使用选项2,则由于性能原因,除非有event.persist(),否则它将事件设置为null。请参阅event pooling

在以下情况下使用选项2(setCount(c => c + 1))

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

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

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

答案 6 :(得分:0)

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

希望对大家有帮助。 :)

答案 7 :(得分:-1)

对于该用例,两者都很好。传递给setState的函数参数仅在您希望通过区分前一个状态有条件地设置状态时才非常有用(我的意思是,您可以使用围绕setState的调用的逻辑来做到这一点,但我认为它在函数中看起来更干净),或者如果您在无法立即访问先前状态的最新版本的闭包中设置状态,则为

一个例子是类似于事件监听器的东西,由于某种原因,它在安装到窗口时仅绑定一次(无论出于何种原因)。例如

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

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

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