函数组件Datatable输入框失焦:内联写法正常、内部定义组件为何失效
react-table Datatable输入框失焦问题解析与解决方案
问题背景
在基于react-table封装Datatable组件的实际开发中,会遇到一个非常典型的行为差异:
- 失效场景:如果把输入类子组件定义在父组件内部,只要在输入框中键入任意字符,输入框就会自动丢失焦点
- 正常场景:如果将input的JSX代码直接内联写在Datatable的columns配置项中,输入功能可以完全正常运行
- 已经初步定位的失效方向:每次输入触发父组件状态更新时,父组件会触发重渲染,定义在内部的组件会被重新创建,最终导致失焦
基于这个定位,还有两个核心问题需要明确:
- 为什么直接写内联JSX的写法,完全不会出现失焦问题?
- 如何在不把输入代码硬编码在Datatable配置项的前提下,让内部定义组件的写法实现正常输入?
失效场景代码示例
import Datatable from "react-table"; import { useState } from "react"; const Test = () => { const [first, setFirst] = useState(""); const [second, setSecond] = useState(""); const Field1 = () => ( <input defaultValue={first} onChange={(e) => setFirst(e.currentTarget.value)} /> ); const Field2 = () => ( <input defaultValue={second} onChange={(e) => setSecond(e.currentTarget.value)} /> ); return ( <Datatable noText pageSize={1} showPagination={false} data={[{ userName: "asdas", email: "asdsad" }]} columns={[ { Header: "www", accessor: "userName", Cell: <Field1 /> }, { Header: "Email", accessor: "email", Cell: <Field2 /> } ]} /> ); }; export default Test;
正常场景代码示例
import Datatable from "react-table"; import { useState } from "react"; const Test = () => { const [first, setFirst] = useState(""); const [second, setSecond] = useState(""); return ( <Datatable noText pageSize={1} showPagination={false} data={[{ userName: "asdas", email: "asdsad" }]} columns={[ { Header: "www", accessor: "userName", Cell: ( <input defaultValue={first} onChange={(e) => setFirst(e.currentTarget.value)} /> ) }, { Header: "Email", accessor: "email", Cell: ( <input defaultValue={second} onChange={(e) => setSecond(e.currentTarget.value)} /> ) } ]} /> ); }; export default Test;
核心问题解答
1. 内联JSX写法不会失焦的底层原因
这个差异本质上是由React的DOM Diff协调机制决定的,和受控/非受控组件的写法没有关系:
- 第一种失效写法里,每次父组件重渲染,都会生成一个全新的函数组件类型
Field1/Field2。React在做组件树对比时,只要发现节点的类型和上一次渲染不一致,就会直接卸载旧的DOM节点,挂载全新的input节点。新挂载的节点根本没有之前的聚焦状态,自然输入一个字符就会失焦。 - 第二种内联写法里,columns配置项的
Cell属性存储的是已经渲染完成的原生input的React元素。每次父组件重渲染生成的新input元素,节点类型始终是固定的原生input,React在做Diff时会执行原地更新逻辑:只会修改节点的属性、文本内容,不会卸载重建DOM节点,因此可以完整保留输入框的聚焦状态。
2. 非硬编码场景的可行实现方式
不需要把输入逻辑硬写在columns配置里,只要保证传给react-table的组件引用稳定,就可以避免重渲染时的组件重建,常见的实现方案有三种:
方案一:将输入子组件定义移到父组件外部
只要组件定义不放在父组件的渲染函数内部,组件的引用就是全局固定的,不会随父组件重渲染重新创建。原本组件需要用到的父组件状态、状态修改方法,通过props传入即可:
// 组件定义在父组件外部,引用全局固定 const Field1 = ({ value, onChange }) => ( <input defaultValue={value} onChange={(e) => onChange(e.currentTarget.value)} /> ); const Field2 = ({ value, onChange }) => ( <input defaultValue={value} onChange={(e) => onChange(e.currentTarget.value)} /> ); const Test = () => { const [first, setFirst] = useState(""); const [second, setSecond] = useState(""); return ( <Datatable noText pageSize={1} showPagination={false} data={[{ userName: "asdas", email: "asdsad" }]} columns={[ { Header: "www", accessor: "userName", Cell: <Field1 value={first} onChange={setFirst} /> }, { Header: "Email", accessor: "email", Cell: <Field2 value={second} onChange={setSecond} /> } ]} /> ); };
方案二:使用useMemo缓存渲染好的输入元素
如果因为作用域依赖的原因,必须把输入逻辑写在父组件内部,可以把渲染好的输入元素用useMemo缓存,避免每次渲染生成全新的组件类型引用:
import { useMemo } from "react"; const Test = () => { const [first, setFirst] = useState(""); const [second, setSecond] = useState(""); // 缓存生成的输入元素,依赖项变化时才会重新生成 const field1El = useMemo(() => ( <input defaultValue={first} onChange={(e) => setFirst(e.currentTarget.value)} /> ), [first]); const field2El = useMemo(() => ( <input defaultValue={second} onChange={(e) => setSecond(e.currentTarget.value)} /> ), [second]); return ( <Datatable noText pageSize={1} showPagination={false} data={[{ userName: "asdas", email: "asdsad" }]} columns={[ { Header: "www", accessor: "userName", Cell: field1El }, { Header: "Email", accessor: "email", Cell: field2El } ]} /> ); };
方案三:利用react-table的Cell渲染函数特性
react-table的Cell配置项本身支持传入接收单元格信息的渲染函数,只要用useCallback固定这个函数的引用,函数内部的抽离逻辑不会触发组件类型变化,非常适配需要动态渲染单元格的业务场景。
内容的提问来源于stack exchange,提问作者otto
相关产品推荐
相关产品推荐

