使用初始状态反应 useState 挂钩事件处理程序

React useState hook event handler using initial state

我仍然在思考 React Hooks,但很难看到我在这里做错了什么。我有一个用于调整面板大小的组件,边缘的 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

useRef 读取未来值

目前,您的问题是您正在读取过去的值。当您定义 handleResize 它属于该渲染时,因此,当您重新渲染时,事件侦听器没有任何反应,因此它仍然从其渲染中读取旧值。

要解决此问题,您应该使用通过 useRef 的引用,并保持更新以便您可以读取当前值。

Example (link to jsfiddle):

  const [activePoint, _setActivePoint] = React.useState(null);

  // define a ref
  const activePointRef = React.useRef(activePoint);

  // in place of original `setActivePoint`
  const setActivePoint = x => {
    activePointRef.current = x; // keep updated
    _setActivePoint(x);
  };

  const handleResize = () => {
    // now when reading `activePointRef.current` you'll
    // have access to the current state
    console.log(activePointRef.current);
  };

  const resizerMouseDown = /* still the same */;

  return /* return is still the same */

您可以从 setter 函数访问当前状态,因此您可以:

const handleResize = () => {
  setActivePoint(activePoint => {
    console.log(activePoint);
    return activePoint;
  })
};
  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
    });
  };

您可以使用 useEffect 挂钩并在每次 activePoint 更改时初始化事件侦听器。通过这种方式,您可以最大限度地减少代码中不必要的引用的使用。

只是对 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);
};

使用typescript的可以使用这个功能:

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 和状态。

useRef回调

除了 ChrisBrownie55 建议的正确方法外,您还可以采用类似的方法,这种方法可能更易于维护,方法是对 eventListener 的回调本身使用 useRef,而不是 useState值。

通过这种方式,您不必担心在以后每 useState 个要使用的参考文献中保存。

只需将 handleResize 保存在 ref 中并在每次渲染时更新其值:

const handleResizeRef = useRef(handleResize)
handleResizeRef.current = handleResize;

并使用 handleResizeRef 作为回调,包裹在箭头函数中:

window.addEventListener('mousemove', e => handleResizeRef.current(e));

沙盒示例

https://codesandbox.io/s/Whosebug-55265255-answer-xe93o?file=/src/App.js

使用自定义挂钩的完整代码:

/* 
this custom hook creates a ref for fn, and updates it on every render. 
The new value is always the same fn, 
but the fn's context changes on every render
*/
const useRefEventListener = fn => {
  const fnRef = useRef(fn);
  fnRef.current = fn;
  return fnRef;
};

export default memo(() => {
  const [activePoint, setActivePoint] = useState(null);

  const handleResize = () => {
    console.log(activePoint);
  };

  // use the custom hook declared above
  const handleResizeRef = useRefEventListener(handleResize)

  const resizerMouseDown = (e, point) => {
    setActivePoint(point);
    // use the handleResizeRef, wrapped by an arrow function, as a callback
    window.addEventListener('mousemove', e => handleResizeRef.current(e));
    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>
  );
});