将 React hooks 与 TypeScript 一起使用,这是我想要做的事情的最小表示:在屏幕上有一个按钮列表,当用户单击按钮时,我想将按钮的文本更改为“单击按钮”并且然后只重新渲染被单击的按钮.
我使用 useCallback 来包装按钮单击事件,以避免在每次渲染时重新创建单击处理程序。
这段代码按照我想要的方式工作:如果我使用 useState 并在数组中维护我的状态,那么我可以使用功能更新 https://reactjs.org/docs/hooks-reference.html#functional-updates在 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:
但这是我的问题:在我的生产应用程序中,状态保存在一个对象中,我们使用 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;
This is how it looks:
所以我的问题归结为:当在 useCallback 中使用 useState 时,我们可以使用功能更新模式并捕获当前状态(而不是从 useCallback 被缓存时获取)
无需指定 useCallback 的依赖关系,这是可能的。
使用 useReducer 时我们如何做同样的事情?使用 useReducer 时,有没有办法获取 useCallback 中的当前状态,而不指定 useCallback 的依赖项?
您可以调度一个将由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
相反,在这种情况下。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)