繁体   English   中英

使用 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

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

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

如果有人正在搜索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'
         }));

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

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

有什么区别?

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

它们都在toggle处理程序中切换propA/B 他们e.target.name更新了一个作为e.target.name传递的道具。

看看当你只更新setMyState一个属性时它会产生什么不同。

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

你可以跟着

编辑 nrrjqj30wp

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

错误演示

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

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


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

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

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 中那样通过扩展先前状态来返回一个新对象。

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

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

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

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

第一:各个州是否相关

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

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

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

在上述情况下,最好使用useReducer进行状态定义。 当您尝试创建例如要在不同交互中updatecreatedelete元素的待办事项应用程序时,这种情况非常常见。

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

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

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

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

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

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

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

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

这两个选项都是有效的,但它们确实有所不同。 使用选项 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 会议。

我要提出的解决方案比上面的解决方案更简单、更容易避免混乱,并且useState API具有相同的用法

使用 npm 包use-merge-state此处 将其添加到您的依赖项中,然后像这样使用它:

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 hook 来管理复杂状态非常方便,而不是 od useState 您可以像这样初始化状态和更新函数:

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

然后你就可以通过只传递部分更新来更新你的状态:

updateState({ ocupation: "postman" })

暂无
暂无

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

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