【问题标题】:React useState hook event handler using initial state使用初始状态反应 useState 钩子事件处理程序
【发布时间】:2019-08-11 09:53:28
【问题描述】:

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

【问题讨论】:

  • 如果你做console.log(point),点的价值是多少,就在setActivePoint(point)之上。我想知道 resizerMouseDown 它是否被调用了。
  • 您的 .addEventListener( 调用似乎正在使用该渲染中的当前函数,因此,当您更改 activePoint 并重新渲染时,这些事件侦听器不会更新为指向新的 activePoint

标签: javascript reactjs ecmascript-6 react-hooks


【解决方案1】:

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 */

【讨论】:

  • 这个解决方案太吵了。我喜欢改用 setActivePoint 方法(见其他答案)
  • @derekliang OP 不想在handleResize 上发送setActivePoint。这会导致屏幕更新过多。这样,setActivePoint 仅在 resizerMouseDown 上调用一次,但 handleResize 的登录与重新渲染无关。
  • 如果状态不改变,就不会更新屏幕。它会导致状态变化检查但看不到变化,因此屏幕没有更新。
  • 我说错了,我的意思是它会强制更新 React。虽然 React 知道不重新渲染到 DOM,但执行数百/数千次检查以查看自从 handleResize 附加到 mousemove 事件后是否没有任何变化仍然很昂贵。
  • 我和OP有同样的问题,但成倍增加。我有几个事件处理程序,它们使用几个不同的状态,它们调用几个其他内部函数。这是在一个以前基于类的组件上,我正在切换到功能,因为我想要useEffect。无论如何,整个情况对我来说感觉很糟糕。令人惊讶的是,一个状态可能完全能够获得旧值。 useRef + statePiece.current 修复仍然是最佳解决方案吗?
【解决方案2】:

你可以从 setter 函数访问当前状态,所以你可以做到:

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

【讨论】:

  • 这不是公认答案的首选解决方案是否有原因?非常简洁。还有为什么这行得通?它如何解决范围界定问题?
  • @aturc setState 支持函数更新:如果将函数传递给 setState,它将接收当前状态作为第一个参数。该函数必须返回一个新状态。如果新状态与旧状态相同,则跳过渲染。所以它可以用来做副作用。 reactjs.org/docs/hooks-reference.html#functional-updates
  • 不确定这是否是好的解决方案。 setState 不应该运行任意代码。
  • 在使用了ref 方法(定义了你自己的状态设置器函数也更新了一个引用)之后,我开始更喜欢这种方法,特别是在带有useEffect 的功能组件中(或具有依赖数组的类似钩子)。主要是因为如果你将ref 方法与任何useEffects 一起使用,你最终不得不将你的setter 函数传递给useEffect,我觉得这很烦人。
【解决方案3】:
  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
    });
  };

【讨论】:

    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

      • 你知道这在性能(或其他方面)是否有任何缺点吗?
      【解决方案6】:

      对于那些使用打字稿的人,你可以使用这个功能:

      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 和 state。

      【讨论】:

        【解决方案7】:

        useRef 用于回调

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

        通过这种方式,您不必担心在将来要使用的每个 useState 中保存一个引用。

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

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

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

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

        沙盒示例

        https://codesandbox.io/s/stackoverflow-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>
          );
        });
        

        【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-04-19
        • 2020-11-30
        • 2020-05-19
        • 1970-01-01
        • 2017-07-16
        • 1970-01-01
        相关资源
        最近更新 更多