React事件处理器间状态共享问题:selectedElement无法获取最新值
问题:React事件监听器无法获取useState更新后的最新状态
以下代码实现了按文本搜索页面元素并将第一个匹配元素设为选中项的功能:
import { useEffect, useState, ChangeEvent } from "react"; function App() { const [inputValue, setInputValue] = useState("Initial value"); const [elements, setElements] = useState<HTMLElement[]>([]); const [selectedElement, setSelectedElement] = useState<HTMLElement | null>( null ); function findElementsByText(selectors: string, text: string) { if (text === "") return []; const regex = new RegExp(text); const elements = Array.from( document.querySelectorAll<HTMLElement>(selectors) ); return elements.filter((element) => { return element.textContent?.match(regex); }); } function handleInputChange(event: ChangeEvent<HTMLInputElement>) { const selectors = "abbr"; const { value } = event.target as HTMLInputElement; const foundElements = findElementsByText(selectors, value); const foundSelectedElement = foundElements[0] || null; setInputValue(value); setElements(foundElements); console.log("selectedElement from handleInputChange", foundSelectedElement); setSelectedElement(foundSelectedElement); } function isCommand(event: KeyboardEvent) { return event.ctrlKey || event.key === "Enter" || event.key === "Escape"; } function handleDocumentKeyDown(event: any) { if (!isCommand(event)) return; if (event.ctrlKey && event.key === "]") { console.log("selectedElement from handleDocumentKeyDown", selectedElement); } } useEffect(() => { document.addEventListener("keydown", handleDocumentKeyDown); return () => { document.removeEventListener("keydown", handleDocumentKeyDown); }; }, []); return ( <div id="keys"> <input type="text" onChange={handleInputChange} value={inputValue} /> <span id="count"> {selectedElement ? elements.indexOf(selectedElement) + 1 : 0}/ {elements.length} </span> <br/> <abbr>a</abbr> <abbr>b</abbr> <abbr>c</abbr> </div> ); } export default App;
实际测试发现:在输入框中输入"a"触发handleInputChange,此时foundSelectedElement为<abbr>a</abbr>并调用setSelectedElement更新状态后,按下快捷键Ctrl + ]触发handleDocumentKeyDown时,打印的selectedElement仍为null,无法获取到更新后的选中元素。需求是通过Ctrl + ]切换到下一个匹配元素,因此需要在handleDocumentKeyDown中获取selectedElement的最新值。
解决方案
问题原因
useEffect的依赖数组为空,导致组件仅在首次渲染时绑定keydown事件监听器,此时handleDocumentKeyDown作为闭包捕获的是首次渲染时的selectedElement(值为null)。后续setSelectedElement触发组件重渲染时,不会重新绑定事件监听器,因此旧的handleDocumentKeyDown始终访问的是旧状态值。
修复方案(两种可选)
方案1:更新useEffect依赖并使用useCallback优化
将handleDocumentKeyDown加入useEffect的依赖数组,同时用useCallback包裹该函数,确保仅在依赖变化时重新创建函数引用,避免不必要的事件解绑/绑定:
import { useEffect, useState, ChangeEvent, useCallback } from "react"; function App() { const [inputValue, setInputValue] = useState("Initial value"); const [elements, setElements] = useState<HTMLElement[]>([]); const [selectedElement, setSelectedElement] = useState<HTMLElement | null>( null ); function findElementsByText(selectors: string, text: string) { if (text === "") return []; const regex = new RegExp(text); const elements = Array.from( document.querySelectorAll<HTMLElement>(selectors) ); return elements.filter((element) => { return element.textContent?.match(regex); }); } function handleInputChange(event: ChangeEvent<HTMLInputElement>) { const selectors = "abbr"; const { value } = event.target as HTMLInputElement; const foundElements = findElementsByText(selectors, value); const foundSelectedElement = foundElements[0] || null; setInputValue(value); setElements(foundElements); console.log("selectedElement from handleInputChange", foundSelectedElement); setSelectedElement(foundSelectedElement); } function isCommand(event: KeyboardEvent) { return event.ctrlKey || event.key === "Enter" || event.key === "Escape"; } // 用useCallback包裹,依赖selectedElement和elements const handleDocumentKeyDown = useCallback((event: any) => { if (!isCommand(event)) return; if (event.ctrlKey && event.key === "]") { console.log("selectedElement from handleDocumentKeyDown", selectedElement); // 实现切换到下一个元素的逻辑 if (elements.length > 0) { const currentIndex = selectedElement ? elements.indexOf(selectedElement) : -1; const nextIndex = (currentIndex + 1) % elements.length; setSelectedElement(elements[nextIndex]); } } }, [selectedElement, elements]); useEffect(() => { document.addEventListener("keydown", handleDocumentKeyDown); return () => { document.removeEventListener("keydown", handleDocumentKeyDown); }; }, [handleDocumentKeyDown]); // 添加依赖 return ( <div id="keys"> <input type="text" onChange={handleInputChange} value={inputValue} /> <span id="count"> {selectedElement ? elements.indexOf(selectedElement) + 1 : 0}/ {elements.length} </span> <br/> <abbr>a</abbr> <abbr>b</abbr> <abbr>c</abbr> </div> ); } export default App;
方案2:使用useRef保存最新状态
利用useRef的current属性可变且不受闭包影响的特性,同步保存selectedElement的最新值:
import { useEffect, useState, ChangeEvent, useRef } from "react"; function App() { const [inputValue, setInputValue] = useState("Initial value"); const [elements, setElements] = useState<HTMLElement[]>([]); const [selectedElement, setSelectedElement] = useState<HTMLElement | null>( null ); // 创建ref同步selectedElement的最新值 const selectedElementRef = useRef<HTMLElement | null>(null); // 每次selectedElement更新时同步ref useEffect(() => { selectedElementRef.current = selectedElement; }, [selectedElement]); function findElementsByText(selectors: string, text: string) { if (text === "") return []; const regex = new RegExp(text); const elements = Array.from( document.querySelectorAll<HTMLElement>(selectors) ); return elements.filter((element) => { return element.textContent?.match(regex); }); } function handleInputChange(event: ChangeEvent<HTMLInputElement>) { const selectors = "abbr"; const { value } = event.target as HTMLInputElement; const foundElements = findElementsByText(selectors, value); const foundSelectedElement = foundElements[0] || null; setInputValue(value); setElements(foundElements); console.log("selectedElement from handleInputChange", foundSelectedElement); setSelectedElement(foundSelectedElement); } function isCommand(event: KeyboardEvent) { return event.ctrlKey || event.key === "Enter" || event.key === "Escape"; } function handleDocumentKeyDown(event: any) { if (!isCommand(event)) return; if (event.ctrlKey && event.key === "]") { // 从ref中获取最新值 console.log("selectedElement from handleDocumentKeyDown", selectedElementRef.current); // 实现切换下一个元素的逻辑 if (elements.length > 0) { const currentIndex = selectedElementRef.current ? elements.indexOf(selectedElementRef.current) : -1; const nextIndex = (currentIndex + 1) % elements.length; setSelectedElement(elements[nextIndex]); } } } useEffect(() => { document.addEventListener("keydown", handleDocumentKeyDown); return () => { document.removeEventListener("keydown", handleDocumentKeyDown); }; }, [elements]); // elements变化时更新监听器,因为切换逻辑依赖elements return ( <div id="keys"> <input type="text" onChange={handleInputChange} value={inputValue} /> <span id="count"> {selectedElement ? elements.indexOf(selectedElement) + 1 : 0}/ {elements.length} </span> <br/> <abbr>a</abbr> <abbr>b</abbr> <abbr>c</abbr> </div> ); } export default App;
内容的提问来源于stack exchange,提问作者wyc
相关产品推荐
相关产品推荐

