React使用useRef数组实现Todo列表焦点控制异常排查
React Todo 应用焦点控制问题修复
核心问题排查
你当前实现的Add按钮焦点异常由两个直接原因导致:
- React状态更新是异步批处理机制,调用
setMyRows后不会立刻同步渲染DOM,此时执行focus逻辑根本拿不到新生成的输入框DOM引用 - 焦点索引计算使用了更新前的旧状态值:首次点击Add时初始列表长度为0,计算出的目标索引为
0-1=-1,自然无响应;后续每次点击都取旧数组长度计算索引,焦点永远滞后一位,落在上一次新增的输入框上 - 上移、下移、删除操作未实现对应焦点控制逻辑,不满足你列出的交互规则
修复后完整实现
import React, { useState, useRef, useEffect } from "https://cdn.skypack.dev/react@17.0.1"; import ReactDOM from "https://cdn.skypack.dev/react-dom@17.0.1"; const App = () => { const [myRows, setMyRows] = useState([]); const focusInput = useRef([]); // 记录需要聚焦的目标索引,DOM更新后统一执行聚焦 const [focusTarget, setFocusTarget] = useState(null); // 生成唯一行ID,避免删除后新增ID重复导致的渲染异常 const nextId = useRef(0); // DOM更新完成后自动执行聚焦 useEffect(() => { if (focusTarget !== null && focusInput.current[focusTarget]) { focusInput.current[focusTarget].focus(); setFocusTarget(null); } }, [focusTarget, myRows]); const onAddRow = () => { setMyRows((prevRows) => { return [ ...prevRows, { id: nextId.current++, text: "", up: "↑", down: "↓", delete: "X" }, ]; }); // 新增项在列表最后一位,索引等于新增前列表长度 setFocusTarget(myRows.length); }; const onMoveUp = (index) => () => { const currentState = [...myRows]; if (index !== 0) { // 交换相邻项 [currentState[index - 1], currentState[index]] = [currentState[index], currentState[index - 1]]; setMyRows(currentState); // 上移后原位置-1为目标位置 setFocusTarget(index - 1); } else { // 已经在顶部,直接聚焦第一项 setFocusTarget(0); } }; const onMoveDown = (index) => () => { const currentState = [...myRows]; if (index !== myRows.length - 1) { // 交换相邻项 [currentState[index], currentState[index + 1]] = [currentState[index + 1], currentState[index]]; setMyRows(currentState); // 下移后原位置+1为目标位置 setFocusTarget(index + 1); } else { // 已经在底部,直接聚焦当前项 setFocusTarget(index); } }; const onDelete = (index) => () => { const currentState = [...myRows]; currentState.splice(index, 1); setMyRows(currentState); // 删完无剩余项不聚焦 if (currentState.length === 0) return; // 删的是最后一项,聚焦新的最后一项;否则聚焦顶替到当前位置的项 setFocusTarget(index === currentState.length ? index - 1 : index); }; const onTextUpdate = (index) => (event) => { setMyRows((prevState) => { const data = [...prevState]; data[index] = { ...data[index], text: event.target.value, }; return data; }); }; return ( <div className="container"> <button onClick={onAddRow}>Add</button> <br /> {myRows.map((row, index) => { return ( <div key={row.id}> <input ref={(el) => (focusInput.current[index] = el)} onChange={onTextUpdate(index)} value={row.text} type="text" /> <button onClick={onMoveUp(index)}>{row.up}</button> <button onClick={onMoveDown(index)}>{row.down}</button> <button onClick={onDelete(index)}>{row.delete}</button> </div> ); })} </div> ); } ReactDOM.render(<App />, document.getElementById("root"))
关键修改点说明
- 新增
focusTarget状态与配套useEffect:所有操作只需要设置需要聚焦的索引值,等React完成DOM渲染更新后,统一执行聚焦逻辑,彻底解决异步状态更新导致的ref引用不存在、焦点滞后问题 - 修正所有操作的焦点索引计算逻辑,完全匹配提出的4条焦点规则
- 替换原有的长度生成ID逻辑为自增唯一ID,避免列表项增删后key重复导致的DOM渲染错位问题
- 补全上移、下移、删除操作的焦点控制分支,覆盖边界场景(顶部上移、底部下移、删除最后一项、删完列表为空)
内容的提问来源于stack exchange,提问作者Ashar
相关产品推荐
相关产品推荐

