我仍然在思考反应钩子,但很难看到我在这里做错了什么。我有一个用于调整面板大小的组件,onmousedown
边缘,我在状态上更新一个值,然后有一个使用该值的mousemove
的事件处理程序,但是在值更改后它似乎没有更新。
这是我的代码:
export default memo(() => {
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
console.log(activePoint); // is null but should be 'top|bottom|left|right'
};
const resizerMouseDown = (e, point) => {
setActivePoint(point); // setting state as 'top|bottom|left|right'
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', cleanup); // removed for clarity
};
return (
<div className="interfaceResizeHandler">
{resizePoints.map(point => (
<div
key={ point }
className={ `interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${ point }` }
onMouseDown={ e => resizerMouseDown(e, point) }
/>
))}
</div>
);
});
问题出在handleResize
函数上,这应该使用最新版本的activePoint
,这将是一个字符串top|left|bottom|right
,而是null
。
如何修复过时的useState
目前,您的问题是您正在读取过去的值。当您定义它属于该渲染handleResize
时,因此,当您重新渲染时,事件侦听器不会发生任何变化,因此它仍然从其渲染中读取旧值。
有几种方法可以解决这个问题。首先,让我们看一下最简单的解决方案。
在作用域中创建函数
鼠标按下事件的事件侦听器将point
值传递给resizerMouseDown
函数。该值与您activePoint
设置的值相同,因此您可以将handleResize
函数的定义移动到resizerMouseDown
和console.log(point)
中。由于此解决方案非常简单,因此无法解释需要在另一个上下文中resizerMouseDown
之外访问状态的情况。
在 CodeSandbox 上实时查看范围内的函数解决方案。
读取未来值useRef
更通用的解决方案是创建一个useRef
,只要activePoint
发生变化,您就可以从任何过时的上下文中读取当前值。
const [activePoint, _setActivePoint] = React.useState(null);
// Create a ref
const activePointRef = React.useRef(activePoint);
// And create our custom function in place of the original setActivePoint
function setActivePoint(point) {
activePointRef.current = point; // Updates the ref
_setActivePoint(point);
}
function handleResize() {
// Now you'll have access to the up-to-date activePoint when you read from activePointRef.current in a stale context
console.log(activePointRef.current);
}
function resizerMouseDown(event, point) {
/* Truncated */
}
在 CodeSandbox 上实时查看useRef
解决方案。
补遗
应该注意的是,这些不是解决此问题的唯一方法,但这些是我的首选方法,因为尽管某些解决方案比提供的其他解决方案更长,但逻辑对我来说更清晰。请使用您和您的团队最了解和找到的解决方案,以最好地满足您的特定需求;不过,不要忘记记录您的代码的作用。
您可以从 setter 函数访问当前状态,因此您可以:
const handleResize = () => {
setActivePoint(activePoint => {
console.log(activePoint);
return activePoint;
})
};
调的useRef
可以采用与 Andria's 类似的方法,方法是使用useRef
更新事件侦听器的回调本身而不是useState
值。这允许您在一个只有一个useRef
的回调中使用许多最新的useState
值。
如果使用useRef
创建 ref 并将其值更新为每次渲染时的handleResize
回调,则存储在 ref 中的回调将始终可以访问最新的useState
值,并且任何过时的回调(如事件处理程序)都可以访问handleResize
回调。
function handleResize() {
console.log(activePoint);
}
// Create the ref,
const handleResizeRef = useRef(handleResize);
// and then update it on each re-render.
handleResizeRef.current = handleResize;
// After that, you can access it via handleResizeRef.current like so
window.addEventListener("mousemove", event => handleResizeRef.current());
考虑到这一点,我们还可以将 ref 的创建和更新抽象为自定义钩子。
例
在CodeSandbox上观看直播。
/**
* A custom hook that creates a ref for a function, and updates it on every render.
* The new value is always the same function, but the function's context changes on every render.
*/
function useRefEventListener(fn) {
const fnRef = useRef(fn);
fnRef.current = fn;
return fnRef;
}
export default memo(() => {
const [activePoint, setActivePoint] = useState(null);
// We can use the custom hook declared above
const handleResizeRef = useRefEventListener((event) => {
// The context of this function will be up-to-date on every re-render.
console.log(activePoint);
});
function resizerMouseDown(event, point) {
setActivePoint(point);
// Here we can use the handleResizeRef in our event listener.
function handleResize(event) {
handleResizeRef.current(event);
}
window.addEventListener("mousemove", handleResize);
// cleanup removed for clarity
window.addEventListener("mouseup", cleanup);
}
return (
<div className="interfaceResizeHandler">
{resizePoints.map((point) => (
<div
key={point}
className={`interfaceResizeHandler__resizer interfaceResizeHandler__resizer--${point}`}
onMouseDown={(event) => resizerMouseDown(event, point)}
/>
))}
</div>
);
});
const [activePoint, setActivePoint] = useState(null); // initial is null
const handleResize = () => {
setActivePoint(currentActivePoint => { // call set method to get the value
console.log(currentActivePoint);
return currentActivePoint; // set the same value, so nothing will change
// or a different value, depends on your use case
});
};
只是对敬畏的ChrisBrownie55的建议的一小部分补充。
可以实现自定义钩子以避免重复此代码,并以与标准useState
几乎相同的方式使用此解决方案:
// useReferredState.js
import React from "react";
export default function useReferredState(initialValue) {
const [state, setState] = React.useState(initialValue);
const reference = React.useRef(state);
const setReferredState = value => {
reference.current = value;
setState(value);
};
return [reference, setReferredState];
}
// SomeComponent.js
import React from "react";
const SomeComponent = () => {
const [someValueRef, setSomeValue] = useReferredState();
// console.log(someValueRef.current);
};
对于那些使用打字稿的人,您可以使用这个函数:
export const useReferredState = <T>(
initialValue: T = undefined
): [T, React.MutableRefObject<T>, React.Dispatch<T>] => {
const [state, setState] = useState<T>(initialValue);
const reference = useRef<T>(state);
const setReferredState = (value) => {
reference.current = value;
setState(value);
};
return [state, reference, setReferredState];
};
并这样称呼它:
const [
recordingState,
recordingStateRef,
setRecordingState,
] = useReferredState<{ test: true }>();
当您调用setRecordingState
时,它将自动更新 ref 和状态。
当您需要在组件挂载时添加事件侦听器
use, useEffect() hook
我们需要使用 useEffect 来设置事件侦听器并清理相同的事件。
使用效果依赖项列表需要具有事件处理程序中使用的状态变量。这将确保处理程序不会访问任何过时事件。
请参阅以下示例。我们有一个简单的count
状态,当我们单击给定的按钮时,该状态会递增。Keydown
事件侦听器打印相同的状态值。如果我们从依赖项列表中删除count
变量,我们的事件侦听器将打印旧的 state 值。
import { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(0);
const clickHandler = () => {
console.log({ count });
setCount(c => c + 1);
}
useEffect(() => {
document.addEventListener('keydown', normalFunction);
//Cleanup function of this hook
return () => {
document.removeEventListener('keydown', normalFunction);
}
}, [count])
return (
<div className="App">
Learn
<button onClick={clickHandler}>Click me</button>
<div>{count}</div>
</div>
);
}
export default App;
您可以使用 useEffect 钩子,并在每次 activePoint 更改时初始化事件侦听器。通过这种方式,您可以最大限度地减少代码中不必要的引用的使用。