使用useReducer时如何在useCallback中获取当前状态?

时间:2020-05-01 10:05:34

标签: javascript reactjs typescript react-hooks

使用带有TypeScript的react钩子,这是我要做的事情的最小表示:屏幕上有一个按钮列表,当用户单击按钮时,我想将按钮的文本更改为“ Button”点击”,然后仅重新呈现被点击的按钮

我正在使用useCallback来包装按钮click事件,以避免在每个渲染器上重新创建单击处理程序。

此代码按我想要的方式工作:如果我使用useState并将状态保持在数组中,则可以在useState中使用Functional update并获得所需的确切行为:

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;

这是所需的结果: enter image description here

但这是我的问题:在我的生产应用中,状态保留在一个对象中,我们使用useReducer钩子模拟类组件样式setState,我们只需要在其中传递更改的属性。因此,我们不必每次操作都替换整个状态。

当尝试使用useReducer进行与以前相同的操作时,状态始终是陈旧的,因为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;

外观如下: enter image description here

所以我的问题归结为::在useCallback中使用useState时,我们可以使用功能更新模式并捕获当前状态(而不是从缓存useCallback时开始) 无需指定useCallback依赖项就可以实现。

使用useReducer时我们该如何做?在使用useReducer时并且没有指定useCallback依赖项时,是否可以在useCallback中获取当前状态?

1 个答案:

答案 0 :(得分:3)

您可以调度一个由reducer调用的函数,并获取传递给它的当前状态。像这样:

//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的原因。

相关问题