簡體   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