React useState hook 事件处理程序使用初始状态



我仍然在思考反应钩子,但很难看到我在这里做错了什么。我有一个用于调整面板大小的组件,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函数的定义移动到resizerMouseDownconsole.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 更改时初始化事件侦听器。通过这种方式,您可以最大限度地减少代码中不必要的引用的使用。

相关内容

  • 没有找到相关文章

最新更新