简体   繁体   English

使用useReducer时如何在useCallback中获取当前的state?

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

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 .使用带有 TypeScript 的反应钩子,这是我正在尝试做的最小表示:在屏幕上有一个按钮列表,当用户单击一个按钮时,我想将按钮的文本更改为“按钮单击”和然后只重新渲染被点击的按钮

I am using useCallback for wrapping the button click event to avoid the click handler getting re-created on every render.我正在使用 useCallback 来包装按钮单击事件,以避免在每次渲染时重新创建单击处理程序。

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:此代码按我想要的方式工作:如果我使用 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;

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 component style setState where we only need to pass in the changed properties.但这是我的问题:在我的生产应用程序中,state 维护在 object 中,我们使用 useReducer 挂钩来模拟 class 组件样式 setState ,我们只需要更改属性。 So we don't have to keep replacing the entire state for every action.所以我们不必为每个动作都更换整个 state。

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.当尝试使用 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;

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.所以我的问题归结为:在 useCallback 中使用 useState 时,我们可以使用功能更新模式并捕获当前的 state(而不是从缓存 useCallback 时开始)这可以在不指定对 useCallback 的依赖项的情况下进行。

How can we do the same thing when using useReducer?使用 useReducer 时我们如何做同样的事情? Is there a way to get the current state inside useCallback when using useReducer and without specifying dependencies to useCallback?有没有办法在使用 useReducer 并且不指定 useCallback 的依赖项时在 useCallback 中获取当前的 state?

You can dispatch a function that will be called by the reducer and gets the current state passed to it.您可以发送一个 function 将由减速器调用并获取当前 state 传递给它。 Something like this:像这样的东西:

//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
  },
  []
);

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.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>

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.这不是您通常使用useReducer的方式,也不是在这种情况下您不会只使用useState的理由。

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

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