【ReactJS】包含复杂表单元素的长列表最佳实践

2026-03-20 00:34:34

长列表中每一行如果包含表单元素或者图片等复杂组件时,修改某一行文本框,会发现非常卡顿。

如下示例代码,长列表有 500 行,修改第一行文本框,会发现每次即便修改一个字符,所有行都会重新渲染一遍 (控制台会打印 500 次 “Row”),这是导致卡顿的根本原因。

import React, { useState } from 'react'

// 构造 500 条数据 Array<{ id: number, name: string }>
const data = Array.from({ length: 500 }, (_, index) => ({ id: index + 1, name: String(Math.random()) }))

const Table = () => {
    const [ dataSource, setDataSource ] = useState(data)

    const onChange = (id, value) => {
        setDataSource(dataSource.map(d => {
            if (d.id === id) {
                d.name = value
            }
            return d
        }))
    }

    return (
        <div>
            {
                dataSource.map(rowData => {
                    return (
                        <Row 
                            key={rowData.id}
                            rowData={rowData}
                            onChange={onChange}
                        />
                    )
                })
            }
        </div>
    )
}

const Row = ({
    rowData,
    onChange,
}: {
    rowData: { id: number; name: string; };
    onChange: (id: number, value: string) => void;
}) => {
    console.log('Row');

    return (
        <div>
            <span>{rowData.id}</span>
            <input 
                value={rowData.name} 
                onChange={e => onChange?.(rowData.id, e.target.value)}
            />
        </div>
    )
}

export default TestPage

我们期待的结果是: 只操作某一行内的文本框时,其它行不要进行渲染。

给 Row 组件套上 memo(),此时只要 rowData 和 onChange 不变化,就不会重新渲染的。

比如修改第一行文本框数据,其它行的 rowData 是不会变化的,这非常容易理解。

但是 onChange 是个函数,每次修改第一行文本框数据时,onChange 这个函数的函数引用会重新生成。

const Row = memo(({
    rowData,
    onChange,
}: any) => {
    console.log('Row');
    return (
        <div>
            <span>{rowData.id}</span>
            <input 
                value={rowData.name} 
                onChange={e => onChange?.(e.target.value)}
            />
        </div>
    )
})

修改 Table 组件,用 useCallback 钩子包裹 onChange 函数:

const Table = () => {
    const [ dataSource, setDataSource ] = useState(data)

    const onChange = useCallback((id, value) => {
        setDataSource(dataSource.map(d => {
            if (d.id === id) {
                d.name = value
            }
            return d
        }))
    }, [])

    return (
        <div>
            {
                dataSource.map(rowData => {
                    return (
                        <Row 
                            key={rowData.id}
                            rowData={rowData}
                            onChange={onChange}
                        />
                    )
                })
            }
        </div>
    )
}

理论上这么改是可以保证 onChange 函数引用不会改变,测试会发现这会儿压根没法修改文本框内的值。

原因是 useCallback 内部构成了闭包,在 useCallback 内部获取的 dataSource 永远是第一次页面渲染时的变量值。

要解决这个问题,我们只需要在 useCallback 内部改变写法即可。

const onChange = useCallback((id, value) => {
    // 旧写法 setDataSource(dataSource)
    // setDataSource(dataSource.map(d => {
    //     if (d.id === id) {
    //         d.name = value
    //     }
    //     return d
    // }))

    // 新写法:回调函数方式 setDataSource(prev => prev)
    setDataSource(prev => prev.map(p => 
        p.id === id ? { ...p, name: value } : p,
    ))
}, [ ])

完整示例如下:

import React, { memo, useCallback, useState } from 'react'

const data = Array.from({ length: 500 }, (_, index) => ({ id: index + 1, name: String(Math.random()) }))

const Table = () => {
    const [ dataSource, setDataSource ] = useState(data)

    const onChange = useCallback((id, value) => {
        setDataSource(prev => {
            return prev.map(p => {
                if (p.id === id) {
                    return { ...p, name: value }
                }
                return p
            })
        })
    }, [])

    return (
        <div>
            {
                dataSource.map(rowData => {
                    return (
                        <Row 
                            key={rowData.id}
                            rowData={rowData}
                            onChange={onChange}
                        />
                    )
                })
            }
        </div>
    )
}

const Row = memo(({
    rowData,
    onChange,
}: {
    rowData: { id: number; name: string; };
    onChange: (id: number, value: string) => void;
}) => {
    console.log('Row');
    return (
        <div>
            <span>{rowData.id}</span>
            <input 
                value={rowData.name} 
                onChange={e => onChange?.(rowData.id, e.target.value)}
            />
        </div>
    )
})

export default Table

当然了,对于长列表也可以做成虚拟列表的方式来优化性能,动态加载视口内的元素,参考 《reactjs react-intersection-observer 实现虚拟列表》

返回首页

本文总阅读量  次
皖ICP备17026209号-3
总访问量: 
总访客量: