无法覆盖 JavaScript 变量

3
我有一个React组件,它在用户输入搜索栏时显示“自动完成”建议列表。我正在尝试允许用户使用箭头键和回车键来浏览列表。

enter image description here

当用户第一次按下向下键时,焦点变量增加一,并且HTML焦点转到第一个推荐项。如果他们再次按下它,则焦点变量再次增加一,HTML焦点移动到第二个推荐项,以此类推。同样地,当用户按下向上键时,它会将焦点减少一并将HTML焦点移动到上一个建议。此外,还有措施确保焦点索引不会超出可能的范围,并在搜索栏的值更改时将焦点索引重置为零。
我遇到了这个奇怪的错误,即在覆盖焦点索引值时,仅在使用向下键时才能正常运行,然后当我按向上键时,它将更改为覆盖之前的上一个值。在某些情况下,这可能导致焦点索引超出范围,然后崩溃,因为当您输入更长的搜索时,它会缩小推荐列表。
以下是仅搜索一个字母时控制台输出焦点索引的屏幕截图:

enter image description here enter image description here

现在我将展示当我缩小搜索范围并导航推荐时会发生什么:

enter image description here enter image description here

这里是一个可用的 CodeSandbox 链接:

https://codesandbox.io/s/jolly-tu-7itsm?fontsize=14&hidenavigation=1&theme=dark


1
“focus” 应该被存储在 React ref 中,这样它就可以跨渲染保持不变,并且不会在每次渲染时重置为“-1”。此外,每次 effect 回调被触发时,您都会添加一个新的事件监听器,但是您从未删除它们(根据您的片段所示)。您能否将此代码放入 运行中的 codesandbox 中,以便我们进行实时调试? - Drew Reese
如果您的组件因为任何原因重新渲染,焦点索引都会重置。除非列表大小更新,否则您可能希望它保持不变,并在那时检查边界、重置等。我也很好奇这是什么类型的选择输入,因为我知道的大多数选择输入应该已经支持键盘访问了。 - Drew Reese
@DrewReese 我认为它根本不会渲染,因为依赖项只有autocompleteContainer、value和items。所有这些值都可以以某种方式改变建议。整个程序中还有许多更多的变量,但我没有在这个片段中包含它们。 - JSON_Derulo
在组件函数体内,您有一个 let focus = -1;,所以如果任何事情导致组件重新渲染,比如父组件重新渲染,那么 focus 就会被设回 -1。也许实际上没有任何东西触发了您的组件重新渲染,但是您不应该指望这一点。 - Drew Reese
让我们在聊天中继续这个讨论 - JSON_Derulo
显示剩余2条评论
1个回答

2

问题

好的,正如我所怀疑的那样。您在“keyup”事件处理程序中封装了一些focus值,该值与主组件体和useEffect挂钩在每个渲染周期中重置focus时独立变异。

您的codesandbox甚至有一个反应lint警告:

从React Hook useEffect内部分配给'focus'变量的赋值将在每次呈现后丢失。为了随时间保留值,请在useRef Hook中存储它,并将可变值保存在'.current'属性中。

解决方案

  1. 将您的“focus”值存储在React ref中,以便所有回调实例和挂钩都可以引用相同的稳定值。
  2. 我建议还使用单独的useEffect挂钩来管理“keyup”事件侦听器和清除函数,以在组件卸载时删除它。这也将防止您的效果在每次效果回调触发时添加新侦听器(您的代码每次呈现都会添加侦听器!)。
  3. 我建议还使用React ref引用"autocomplete__container"容器,因为直接DOM查询和操作被认为是反模式。

代码:

function App() {
  const [value, setValue] = useState("");
  const [items, setItems] = useState([]);
  const [query, setQuery] = useState([]);
  const [formSubmit, setFormSubmit] = useState(false);
  const [completions] = useAutocomplete(value, autocompleteValues);

  const autocompleteContainer = useRef(); // <-- ref to get DOMNode
  const focusRef = useRef(-1); // <-- ref to store stable focus value

  ...

  // Effect hook to manage keyup event handler and cleanup
  useEffect(() => {
    const handleKeypress = (event) => {
      if (event.keyCode === 38 && focusRef.current >= 1) { // <-- focus.current
        focusRef.current -= 1;
        autocompleteContainer.current.childNodes[focusRef.current].focus(); // <-- autocompleteContainer.current
      } else if (
        event.keyCode === 40 &&
        focusRef.current < autocompleteContainer.current.childElementCount - 1
      ) {
        focusRef.current += 1;
        autocompleteContainer.current.childNodes[focusRef.current].focus();
      } else if (
        event.keyCode === 13 &&
        document.activeElement.className !== "form-control"
      ) {
        setValue(document.activeElement.innerHTML);
        setQuery(document.activeElement.innerHTML);
        document.getElementsByClassName(
          "autocomplete__container"
        )[0].style.display = "none";
        setFormSubmit(true);
      }
    };

    window.addEventListener("keyup", handleKeypress); // <-- add listener

    return () => window.removeEventListener("keyup", handleKeypress); // <-- return cleanup function to remove listener
  }, []);

  // Effect hook to reset focus value when list updates
  useEffect(() => {
    focusRef.current = -1;
    console.log("focus reset", focusRef.current);
  }, [value, items]);

  return (
    <div className="App">
      <Form id="search__form" onSubmit={handleSubmit}>
        <Form.Group>
          <InputGroup className="mb-3">
            ...
            <div
              ref={autocompleteContainer} // <-- attach DOM ref here
              className="autocomplete__container"
            >
              {completions.map((val, index) => (
                <p
                  tabIndex={index + 10}
                  key={index}
                  onClick={() => updateInput({ val })}
                >
                  {val}
                </p>
              ))}
            </div>
          </InputGroup>
        </Form.Group>
      </Form>
      <p>{focusRef.current}</p>
    </div>
  );
}

Edit unable-to-override-javascript-variable


再次感谢您提供的出色解决方案。我看到它在codesandbox中可以正常工作,但是当我在我的机器上更改代码时,会出现“TypeError:无法读取未定义的属性'childElementCount'”,并指向第一个else if。 - JSON_Derulo
如果您选择将“autocompleteContainer”转换为React ref,则它不再是先前由“document.getElementsByClassName”返回的数组。它应该是“autocompleteContainer.current.childElementCount”。 - Drew Reese

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接