如何使用useEffect挂钩注册事件?

Isa*_*aac 24 javascript reactjs react-hooks

我正在学习有关如何使用钩子注册事件的Udemy课程,讲师给出了以下代码:

  const [userText, setUserText] = useState('');

  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${userText}${key}`);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', handleUserKeyPress);

    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };
  });

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
Run Code Online (Sandbox Code Playgroud)

现在效果很好,但我不认为这是正确的方法。原因是,如果我理解正确,那么每次重新渲染时,事件都会每次都在注册和注销,而我根本不认为这样做是正确的方法。

所以我useEffect对下面的钩子做了一些修改

useEffect(() => {
  window.addEventListener('keydown', handleUserKeyPress);

  return () => {
    window.removeEventListener('keydown', handleUserKeyPress);
  };
}, []);
Run Code Online (Sandbox Code Playgroud)

通过使用一个空数组作为第二个参数,使组件只运行一次效果,即模仿componentDidMount。当我尝试结果时,奇怪的是,在我键入的每个键上都没有附加,而是被覆盖了。

我期待setUserText(${userText}${key}); 将新键入的键追加到当前状态并设置为新状态,但是,它会忘记旧状态并用新状态重写。

我们应该在每次重新渲染时注册和注销事件真的是正确的方法吗?

for*_*d04 35

问题

[...] 在每次重新渲染时,事件每次都会继续注册和取消注册,我只是认为这不是正确的方法。

你是对的。useEffect每次渲染时重新启动内部事件处理是没有意义的。

[...] 空数组作为第二个参数,让组件只运行一次效果 [...] 奇怪的是,在我输入的每个键上,不是追加而是覆盖。

这是陈旧的闭包值的问题。

原因:内部使用的函数useEffect应该是依赖项的一部分。您没有设置任何依赖项 ( []),但仍然调用handleUserKeyPress,它本身读取userText状态。

解决方案

根据您的用例,有一些替代方案。

1.状态更新器功能

setUserText(prev => `${prev}${key}`);
Run Code Online (Sandbox Code Playgroud)

? 侵入性最小的方法
? 只能访问自己以前的状态,不能访问其他状态

setUserText(prev => `${prev}${key}`);
Run Code Online (Sandbox Code Playgroud)
const App = () => {
  const [userText, setUserText] = useState("");

  useEffect(() => {
    const handleUserKeyPress = event => {
      const { key, keyCode } = event;

      if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
        setUserText(prev => `${prev}${key}`); // use updater function here
      }
    };

    window.addEventListener("keydown", handleUserKeyPress);
    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []); // still no dependencies

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
Run Code Online (Sandbox Code Playgroud)

2. useReducer- “作弊模式”

我们可以切换到useReducer并访问当前状态/道具 - 使用与useState.

变体 2a:reducer 函数内部的逻辑

const [userText, handleUserKeyPress] = useReducer((state, event) => {
    const { key, keyCode } = event;
    // isUpperCase is always the most recent state (no stale closure value)
    return `${state}${isUpperCase ? key.toUpperCase() : key}`;  
}, "");
Run Code Online (Sandbox Code Playgroud)

<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
Run Code Online (Sandbox Code Playgroud)
const [userText, handleUserKeyPress] = useReducer((state, event) => {
    const { key, keyCode } = event;
    // isUpperCase is always the most recent state (no stale closure value)
    return `${state}${isUpperCase ? key.toUpperCase() : key}`;  
}, "");
Run Code Online (Sandbox Code Playgroud)

变体 2b:reducer 函数之外的逻辑 - 类似于useState更新函数

const [userText, setUserText] = useReducer((state, action) =>
      typeof action === "function" ? action(state, isUpperCase) : action, "");
// ...
setUserText((prevState, isUpper) => `${prevState}${isUpper ? key.toUpperCase() : key}`);
Run Code Online (Sandbox Code Playgroud)

const App = () => {
  const [isUpperCase, setUpperCase] = useState(false);
  const [userText, handleUserKeyPress] = useReducer((state, event) => {
    const { key, keyCode } = event;
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      // isUpperCase is always the most recent state (no stale closure)
      return `${state}${isUpperCase ? key.toUpperCase() : key}`;
    }
  }, "");

  useEffect(() => {
    window.addEventListener("keydown", handleUserKeyPress);

    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
      <button style={{ width: "150px" }} onClick={() => setUpperCase(b => !b)}>
        {isUpperCase ? "Disable" : "Enable"} Upper Case
      </button>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
Run Code Online (Sandbox Code Playgroud)

? 不需要管理依赖
?访问多个状态和道具
?与useState
?相同的 API 可扩展到更复杂的案例/减速器
?由于内联减速器(有点可忽略),性能略有下降
?稍微增加了减速器的复杂性

3. useRef/ 在可变引用中存储回调

const cbRef = useRef(handleUserKeyPress);
useEffect(() => { cbRef.current = handleUserKeyPress; }); // update after each render
useEffect(() => {
    const cb = e => cbRef.current(e); // then use most recent cb value
    window.addEventListener("keydown", cb);
    return () => { window.removeEventListener("keydown", cb) };
}, []);
Run Code Online (Sandbox Code Playgroud)

const [userText, setUserText] = useReducer((state, action) =>
      typeof action === "function" ? action(state, isUpperCase) : action, "");
// ...
setUserText((prevState, isUpper) => `${prevState}${isUpper ? key.toUpperCase() : key}`);
Run Code Online (Sandbox Code Playgroud)
const App = () => {
  const [isUpperCase, setUpperCase] = useState(false);
  const [userText, setUserText] = useReducer(
    (state, action) =>
      typeof action === "function" ? action(state, isUpperCase) : action,
    ""
  );

  useEffect(() => {
    const handleUserKeyPress = event => {
      const { key, keyCode } = event;
      if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
        setUserText(
          (prevState, isUpper) =>
            `${prevState}${isUpper ? key.toUpperCase() : key}`
        );
      }
    };

    window.addEventListener("keydown", handleUserKeyPress);
    return () => {
      window.removeEventListener("keydown", handleUserKeyPress);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
      <button style={{ width: "150px" }} onClick={() => setUpperCase(b => !b)}>
        {isUpperCase ? "Disable" : "Enable"} Upper Case
      </button>
    </div>
  );
}


ReactDOM.render(<App />, document.getElementById("root"));
Run Code Online (Sandbox Code Playgroud)

? 对于不用于重新渲染数据流的回调/事件处理程序
?不需要管理依赖
?React docs 只推荐作为最后一个选项
?更必要的方法

查看这些链接以获取更多信息:1 2 3

不恰当的解决方案

useCallback

虽然它可以以各种方式应用,但useCallback不适合这个特定的问题案例

原因:由于添加了依赖项 -userText此处 - 事件侦听器将在每次按键时重新启动,最好的情况是性能不佳,或者更糟的是导致不一致。

<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef } = React</script>
Run Code Online (Sandbox Code Playgroud)
const cbRef = useRef(handleUserKeyPress);
useEffect(() => { cbRef.current = handleUserKeyPress; }); // update after each render
useEffect(() => {
    const cb = e => cbRef.current(e); // then use most recent cb value
    window.addEventListener("keydown", cb);
    return () => { window.removeEventListener("keydown", cb) };
}, []);
Run Code Online (Sandbox Code Playgroud)

为了完整起见,这里有一些useCallback总体上的关键点:

? 万能的务实解决方案
? 微创
? 手动依赖管理
? useCallback使函数定义更加冗长/混乱

在里面声明处理函数 useEffect

直接在内部useEffect声明事件处理函数与 或多或少有相同的问题useCallback,后者只会导致更多的间接依赖关系。

换句话说:useCallback我们没有通过添加额外的依赖层,而是将函数直接放在里面useEffect——但是仍然需要设置所有依赖项,导致频繁的处理程序更改。

事实上,如果你在handleUserKeyPress里面移动useEffect,ESLint 详尽的 deps 规则会告诉你userText,如果没有指定,缺少什么确切的规范依赖项()。

const App = () => {
  const [userText, setUserText] = useState("");

  const handleUserKeyPress = event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${userText}${key}`);
    }
  };

  const cbRef = useRef(handleUserKeyPress);

  useEffect(() => {
    cbRef.current = handleUserKeyPress;
  });

  useEffect(() => {
    const cb = e => cbRef.current(e);
    window.addEventListener("keydown", cb);

    return () => {
      window.removeEventListener("keydown", cb);
    };
  }, []);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
Run Code Online (Sandbox Code Playgroud)
<script src="https://unpkg.com/react@16.13.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script>var { useReducer, useEffect, useState, useRef, useCallback } = React</script>
Run Code Online (Sandbox Code Playgroud)

  • 这开始看起来越来越像钩子中被忽视的设计缺陷 (5认同)
  • @foxtrotuniform6969确实你可能是对的!有一个针对事件相关挂钩的新 RFC(更新的答案) (3认同)
  • https://github.com/reactjs/rfcs/blob/useevent/text/0000-useevent.md (3认同)
  • 谢天谢地,我们需要这个。编辑:顺便说一句,你有链接吗?我想看一眼。 (2认同)

Shu*_*tri 27

处理此类情况的最佳方法是查看事件处理程序中的操作。如果仅使用先前的状态来设置状态,则最好使用回调模式并仅在初始安装时注册事件侦听器。如果您不使用callback patternhttps://reactjs.org/docs/hooks-reference.html#usecallback),则事件侦听器将使用侦听器引用及其词法范围,但会创建一个新函数,并在新的渲染,因此在处理程序中,您将无法进入更新状态

const [userText, setUserText] = useState('');

  const handleUserKeyPress = useCallback(event => {
    const { key, keyCode } = event;

    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(prevUserText => `${prevUserText}${key}`);
    }
  }, []);

  useEffect(() => {
    window.addEventListener('keydown', handleUserKeyPress);

    return () => {
      window.removeEventListener('keydown', handleUserKeyPress);
    };
  }, [handleUserKeyPress]);

  return (
    <div>
      <h1>Feel free to type!</h1>
      <blockquote>{userText}</blockquote>
    </div>
  );
Run Code Online (Sandbox Code Playgroud)

  • 但这将在每次按键时创建一个新的绑定函数。如果您的重点是性能,则局部状态变量会更好 (3认同)
  • 我看到重要的是使用`prevUserText`来引用`userText`的先前状态。如果我需要访问多个状态怎么办?如何获得以前所有状态的访问权限? (2认同)

Spa*_*Bao 11

新答案:

useEffect(() => {
  function handlekeydownEvent(event) {
    const { key, keyCode } = event;
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(prevUserText => `${prevUserText}${key}`);
    }
  }

  document.addEventListener('keyup', handlekeydownEvent)
  return () => {
    document.removeEventListener('keyup', handlekeydownEvent)
  }
}, [])
Run Code Online (Sandbox Code Playgroud)

使用时setUserText,将函数作为参数而不是对象传递,prevUserText将始终是最新状态。


旧答案:

试试这个,它的工作原理与您的原始代码相同:

useEffect(() => {
  function handlekeydownEvent(event) {
    const { key, keyCode } = event;
    if (keyCode === 32 || (keyCode >= 65 && keyCode <= 90)) {
      setUserText(`${userText}${key}`);
    }
  }

  document.addEventListener('keyup', handlekeydownEvent)
  return () => {
    document.removeEventListener('keyup', handlekeydownEvent)
  }
}, [userText])
Run Code Online (Sandbox Code Playgroud)

因为在您的useEffect()方法中,它取决于userText变量,但您不要将它放在第二个参数中,否则userText将始终''使用参数绑定到初始值[]

您不需要这样做,只是想让您知道为什么您的第二个解决方案不起作用。