簡體   English   中英

使用useReducer時如何在useCallback中獲取當前的state?

[英]How to get current state inside useCallback when using useReducer?

使用帶有 TypeScript 的反應鈎子,這是我正在嘗試做的最小表示:在屏幕上有一個按鈕列表,當用戶單擊一個按鈕時,我想將按鈕的文本更改為“按鈕單擊”和然后只重新渲染被點擊的按鈕

我正在使用 useCallback 來包裝按鈕單擊事件,以避免在每次渲染時重新創建單擊處理程序。

此代碼按我想要的方式工作:如果我使用 useState 並將我的 state 維護在一個數組中,那么我可以在 useState 中使用功能更新並獲得我想要的確切行為:

 import * as React from 'react'; import { IHelloWorldProps } from './IHelloWorldProps'; import { useEffect, useCallback, useState } from 'react'; import { PrimaryButton } from 'office-ui-fabric-react'; interface IMyButtonProps { title: string; id: string; onClick: (clickedDeviceId: string) => (event: any) => void; } const MyButton: React.FunctionComponent<IMyButtonProps> = React.memo((props: IMyButtonProps) => { console.log(`Button rendered for ${props.title}`); return <PrimaryButton text={props.title} onClick={props.onClick(props.id)} />; }); interface IDevice { Name: string; Id: string; } const HelloWorld: React.FunctionComponent<IHelloWorldProps> = (props: IHelloWorldProps) => { //If I use an array for state instead of object and then use useState with Functional update, I get the result I want. const initialState: IDevice[] = []; const [deviceState, setDeviceState] = useState<IDevice[]>(initialState); useEffect(() => { //Simulate network call to load data. setTimeout(() => { setDeviceState([{ Name: "Apple", Id: "appl01" }, { Name: "Android", Id: "andr02" }, { Name: "Windows Phone", Id: "wp03" }]); }, 500); }, []); const _deviceClicked = useCallback((clickedDeviceId: string) => ((event: any): void => { setDeviceState(prevState => prevState.map((device: IDevice) => { if (device.Id === clickedDeviceId) { device.Name = `${device.Name} clicked`; } return device; })); }), []); return ( <React.Fragment> {deviceState.map((device: IDevice) => { return <MyButton key={device.Id} title={device.Name} onClick={_deviceClicked} id={device.Id} />; })} </React.Fragment> ); }; export default HelloWorld;

這是期望的結果: 在此處輸入圖像描述

但這是我的問題:在我的生產應用程序中,state 維護在 object 中,我們使用 useReducer 掛鈎來模擬 class 組件樣式 setState ,我們只需要更改屬性。 所以我們不必為每個動作都更換整個 state。

當嘗試使用 useReducer 做與以前相同的事情時,state 總是過時的,因為 useCallback 的緩存版本來自設備列表為空時的第一次加載。

 import * as React from 'react'; import { IHelloWorldProps } from './IHelloWorldProps'; import { useEffect, useCallback, useReducer, useState } from 'react'; import { PrimaryButton } from 'office-ui-fabric-react'; interface IMyButtonProps { title: string; id: string; onClick: (clickedDeviceId: string) => (event: any) => void; } const MyButton: React.FunctionComponent<IMyButtonProps> = React.memo((props: IMyButtonProps) => { console.log(`Button rendered for ${props.title}`); return <PrimaryButton text={props.title} onClick={props.onClick(props.id)} />; }); interface IDevice { Name: string; Id: string; } interface IDeviceState { devices: IDevice[]; } const HelloWorld: React.FunctionComponent<IHelloWorldProps> = (props: IHelloWorldProps) => { const initialState: IDeviceState = { devices: [] }; //Using useReducer to mimic class component's this.setState functionality where only the updated state needs to be sent to the reducer instead of the entire state. const [deviceState, setDeviceState] = useReducer((previousState: IDeviceState, updatedProperties: Partial<IDeviceState>) => ({...previousState, ...updatedProperties }), initialState); useEffect(() => { //Simulate network call to load data. setTimeout(() => { setDeviceState({ devices: [{ Name: "Apple", Id: "appl01" }, { Name: "Android", Id: "andr02" }, { Name: "Windows Phone", Id: "wp03" }] }); }, 500); }, []); //Have to wrap in useCallback otherwise the "MyButton" component will get a new version of _deviceClicked for each time. //If the useCallback wrapper is removed from here, I see the behavior I want but then the entire device list is re-rendered everytime I click on a device. const _deviceClicked = useCallback((clickedDeviceId: string) => ((event: any): void => { //Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here. const updatedDeviceList = deviceState.devices.map((device: IDevice) => { if (device.Id === clickedDeviceId) { device.Name = `${device.Name} clicked`; } return device; }); setDeviceState({ devices: updatedDeviceList }); //Cannot add the deviceState.devices dependency here because we are updating deviceState.devices inside the function. This would mean useCallback would be useless. }), []); return ( <React.Fragment> {deviceState.devices.map((device: IDevice) => { return <MyButton key={device.Id} title={device.Name} onClick={_deviceClicked} id={device.Id} />; })} </React.Fragment> ); }; export default HelloWorld;

這是它的外觀: 在此處輸入圖像描述

所以我的問題歸結為:在 useCallback 中使用 useState 時,我們可以使用功能更新模式並捕獲當前的 state(而不是從緩存 useCallback 時開始)這可以在不指定對 useCallback 的依賴項的情況下進行。

使用 useReducer 時我們如何做同樣的事情? 有沒有辦法在使用 useReducer 並且不指定 useCallback 的依賴項時在 useCallback 中獲取當前的 state?

您可以發送一個 function 將由減速器調用並獲取當前 state 傳遞給它。 像這樣的東西:

//Using useReducer to mimic class component's this.setState functionality where only the updated state needs to be sent to the reducer instead of the entire state.
const [deviceState, dispatch] = useReducer(
  (previousState, action) => action(previousState),
  initialState
);

//Have to wrap in useCallback otherwise the "MyButton" component will get a new version of _deviceClicked for each time.
//If the useCallback wrapper is removed from here, I see the behavior I want but then the entire device list is re-rendered everytime I click on a device.
const _deviceClicked = useCallback(
  (clickedDeviceId) => (event) => {
    //Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here.
    dispatch((deviceState) => ({
      ...deviceState,
      devices: deviceState.devices.map((device) => {
        if (device.Id === clickedDeviceId) {
          device.Name = `${device.Name} clicked`;
        }

        return device;
      }),
    }));
    //no dependencies here
  },
  []
);

下面是一個工作示例:

 const { useCallback, useReducer } = React; const App = () => { const [deviceState, dispatch] = useReducer( (previousState, action) => action(previousState), { count: 0, other: 88 } ); const click = useCallback( (increase) => () => { //Since useCallback contains the cached version of the function before the useEffect runs, deviceState.devices is always an empty array [] here. dispatch((deviceState) => ({...deviceState, count: deviceState.count + increase, })); //no dependencies here }, [] ); return ( <div> <button onClick={click(1)}>+1</button> <button onClick={click(2)}>+2</button> <button onClick={click(3)}>+3</button> <pre>{JSON.stringify(deviceState)}</pre> </div> ); }; ReactDOM.render(<App />, document.getElementById('root'));
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script> <div id="root"></div>

這不是您通常使用useReducer的方式,也不是在這種情況下您不會只使用useState的理由。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM