最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to get current state inside useCallback when using useReducer? - Stack Overflow

programmeradmin6浏览0评论

Using react hooks with TypeScript and here is a minimal representation of what I am trying to do: Have a list of buttons on screen and when the user clicks on a button, I want to change the text of the button to "Button clicked" and then only re-render the button which was clicked.

I am using useCallback for wrapping the button click event to avoid the click handler getting re-created on every render.

This code works the way I want: If I use useState and maintain my state in an array, then I can use the Functional update in useState and get the exact behaviour I want:

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;

Using react hooks with TypeScript and here is a minimal representation of what I am trying to do: Have a list of buttons on screen and when the user clicks on a button, I want to change the text of the button to "Button clicked" and then only re-render the button which was clicked.

I am using useCallback for wrapping the button click event to avoid the click handler getting re-created on every render.

This code works the way I want: If I use useState and maintain my state in an array, then I can use the Functional update in useState and get the exact behaviour I want:

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;

Here is the desired result:

But here is my problem: In my production app, the state is maintained in an object and we are using the useReducer hook to simulate a class ponent style setState where we only need to pass in the changed properties. So we don't have to keep replacing the entire state for every action.

When trying to do the same thing as before with useReducer, the state is always stale as the cached version of useCallback is from the first load when the device list was empty.

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 ponent'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" ponent 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;

This is how it looks:

So my question boils down to this: When using useState inside useCallback, we can use the functional update pattern and capture the current state (instead of from when useCallback was cached) This is possible without specifying dependencies to useCallback.

How can we do the same thing when using useReducer? Is there a way to get the current state inside useCallback when using useReducer and without specifying dependencies to useCallback?

Share Improve this question asked May 1, 2020 at 10:05 Vardhaman DeshpandeVardhaman Deshpande 2042 silver badges9 bronze badges 2
  • useState is the easiest option to mimic class ponents' setState merge behavior. Is there a reason, why you still choose useReducer? – ford04 Commented May 1, 2020 at 11:33
  • Thanks, yes I have given it another thought and decided to go with useState indeed. – Vardhaman Deshpande Commented May 1, 2020 at 15:12
Add a ment  | 

1 Answer 1

Reset to default 5

You can dispatch a function that will be called by the reducer and gets the current state passed to it. Something like this:

//Using useReducer to mimic class ponent'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" ponent 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
  },
  []
);

Below is a working example:

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./ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


<div id="root"></div>

This is not how you would normally use useReducer and don't se a reason why you would not just use useState instead in this instance.

发布评论

评论列表(0)

  1. 暂无评论