在 React 组件中,我面临事件处理程序和自定义挂钩之间状态同步的挑战。事件处理程序更新状态变量,我需要根据此更新的状态访问从自定义挂钩派生的另一个状态的最新值。
我们使用自定义挂钩,并定义状态变量和事件处理程序。
事件处理程序将状态变量
a
的值设置为变量 c
(这取决于 event
参数),然后需要使用 b
的最新值,该值由自定义钩子返回,该值取决于在 a
。
function MyComponent(props) {
const [a, setA] = useState(null);
const b = useCustomHook(a);
const eventHandler = useCallback(async (event) => {
try {
const c = event.data;
setA(c);
if (b) { // we need to use b here, but it is stale (one step behind)
// logic
}
else {
// more logic
}
} catch (e) {
console.error(`Error:`, e);
}
}, [b]);
}
问题在于
b
始终落后于事件处理程序一步。当在事件处理程序中运行逻辑时,其值尚未更新。
如何在事件处理程序内部访问
b
(取决于 a
,取决于 c
,取决于 event
)的最新值?
我尝试仅在事件处理程序内部设置状态变量,并对其余逻辑使用效果,但该效果不仅在触发事件处理程序时运行。
什么解决方案符合 React 范式?如何重构我的代码来解决这个问题?
我建议使用 useEffect hook 和 useRef (以避免渲染问题):
//Code
const latestBRef = useRef(b);
useEffect(() => {
latestBRef.current = b;
}, [b]);
// inside your eventHandler , use latestBRef.current instead of b
从 useCallback 中删除 b 作为依赖项。 如果这没有帮助,请将其放入代码沙箱中,以便我们可以尝试不同的解决方案。
经过大量的头脑风暴、代码编写和调试(在伟大的 ChatGPT 的帮助下),我终于想出了一些似乎可以解决我的问题的代码。它可能设计不精良、简单、高效、不符合 React 的最佳实践等,但它似乎在我的用例中工作正常。
我很高兴获得有关此解决方案的反馈,如果您知道处理此问题的更有效、React 友好的方法,我很想知道!
下面是我的代码的修订版本,旨在为我最初发布的简化问题(使用 a、b 和 c)提供解决方案。
const useCustomHookArgs = ['foo', 'bar']
// useCustomHook needs to be adapted to take a flag based on which it may or may not reset its state.
// I reset its state when using it in conjunction with useEventDependentEffect, but otherwise
// pass shouldResetState = false in the rest of my code
const useCustomHook = (shouldResetState = false, ...useCustomHookArgs) => {
const [b, setB] = useState(null);
const [shouldReset, setShouldReset] = useState(false);
// Use an effect to set b and an internal shouldReset flag based on the shouldResetState arg
useEffect(() => {
const fetchData = async () => {
// Logic to fetch data based on args
const fetchedData = /* fetch logic */;
setB(fetchedData);
if (shouldResetState) {
setShouldReset(true); // Flag needed to reset b after it has been used
}
};
unsubscribeFromStore = subscribeToStore();
fetchData();
return () => {
unsubscribeFromStore();
};
}, [...useCustomHookArgs]);
// Use an effect to reset b (and the shouldReset flag) after it has been used
useEffect(() => {
if (shouldReset) {
setB(null);
setShouldReset(false);
}
}, [shouldReset]);
return b;
};
// useEventDependentEffect is a custom hook that captures the logic needed to
// capture a at the time of the eventHandler call, and then compute b from a
const useEventDependentEffect = (eventHandler, effectFunction, aToCapture, computeB, computeBArgs) => {
const [trigger, setTrigger] = useState(0);
const capturedARef = useRef();
const [capturedA, setCapturedA] = useState(null);
const [computedB, setComputedB] = useState(null);
const shouldResetState = true;
const newComputedB = computeB(shouldResetState, ...computeBArgs, capturedA);
useEffect(() => {
if (newComputedB != null && newComputedB !== computedB) {
setComputedB(newComputedB);
}
}, [newComputedB, computedB]);
// Enhanced event handler to capture a and trigger the effect
const enhancedEventHandler = useCallback((...args) => {
eventHandler(...args);
capturedARef.current = aToCapture.current;
setTrigger(trigger + 1); // Trigger the effect
}, [eventHandler, trigger]);
// Effect that runs the desired logic with the captured a
useEffect(() => {
if (trigger) {
const newCapturedA = capturedARef.current;
// Compare newCapturedA with the current capturedA
if (newCapturedA !== capturedA) {
setCapturedA(newCapturedA);
}
if(computedB != null) {
// Call effectFunction with the computed b to run the logic
// that needed to run below setA(c) in the original problem
effectFunction(computedB, capturedARef.current);
setTrigger(0); // Reset the trigger
setComputedB(null);
setCapturedA(null);
}
}
}, [trigger, effectFunction, capturedA, computeB, computedB]);
return enhancedEventHandler;
};
function MyComponent(props) {
const [a, setA] = useState(null);
const aRef = useRef(a);
const eventHandler = (event) => {
const c = event.data;
setA(c);
aRef.current = c;
};
const effectFunction = async (b, a) => {
try {
if (b) {
// logic using a
} else {
// more logic using a
}
} catch (e) {
console.error(`Error:`, e);
}
};
const aToCapture = {
a: aRef
}
const handleEvent = useEventDependentEffect(eventHandler, effectFunction, aToCapture, useCustomHook, useCustomHookArgs);
// Render logic and event binding using handleEvent
}