React中useSelector与useEffect导致表格重复重渲染的优化问题
问题解决方案
根因分析
当前两次重渲染的原因非常明确:
- 全局store中的searcher更新后,useSelector检测到返回值变化,触发CatalogTable第一次重渲染
- 重渲染完成后useEffect检测到依赖项searcher变更,执行getData请求接口,请求完成后setTableRows更新状态,触发第二次重渲染
解决方案
方案一:移除顶层useSelector,通过手动订阅store避免searcher变更触发渲染
原理:useStore拿到的store实例本身是稳定引用,不会触发组件重渲染,我们手动订阅store变化,仅当searcher真正变化时才发起请求更新tableRows,全程searcher变更不会主动触发组件重渲染,只有setTableRows时才会触发一次渲染。
import { useStore } from 'react-redux' import { shallowEqual } from 'react-redux' const CatalogTable: React.FC<CatalogTableProps> = ({rows}) => { const [tableRows, setTableRows] = React.useState(rows) const store = useStore() const searcherRef = React.useRef(getTableSearcher(store.getState())) const getData = async (currentSearcher) => { const {data} = await CatalogService.getAllCatalogProducts({page: 1, searcher: currentSearcher}) setTableRows(data.products) } React.useEffect(() => { // 订阅store变化,手动比较searcher值 const unsubscribe = store.subscribe(() => { const latestSearcher = getTableSearcher(store.getState()) // 如果searcher是引用类型,这里可以替换为shallowEqual判断 if (latestSearcher && !shallowEqual(latestSearcher, searcherRef.current)) { searcherRef.current = latestSearcher getData(latestSearcher) } }) // 组件卸载时取消订阅 return unsubscribe }, [store]) return ( <> <div className={styles.defaultTable}> <Table headers={headers} label="Products catalog" rows={tableRows} total={total} pagination addButton editButtons searcher getPage={(page: number) => nextPage(page)} type='catalog' /> </div> </> ) } export default CatalogTable
方案二:组件拆分+React.memo隔离渲染
原理:把组件拆分为逻辑层和视图层,逻辑层负责处理搜索监听和数据请求,视图层用memo包裹,仅当传入的tableRows等必要props变化时才触发渲染,逻辑层的多余重渲染不会传递到视图层。
// 视图层组件,仅props变化时重渲染 const CatalogTableView = React.memo(({ tableRows, total, nextPage, headers }) => { return ( <div className={styles.defaultTable}> <Table headers={headers} label="Products catalog" rows={tableRows} total={total} pagination addButton editButtons searcher getPage={nextPage} type='catalog' /> </div> ) }) // 逻辑层组件,处理搜索监听和数据请求 const CatalogTable: React.FC<CatalogTableProps> = ({rows}) => { const [tableRows, setTableRows] = React.useState(rows) const searcher = useSelector(getTableSearcher, shallowEqual) // 用useCallback保证nextPage引用稳定,避免memo失效 const nextPage = useCallback((page: number) => { // 原nextPage逻辑 }, []) const getData = async () => { const {data} = await CatalogService.getAllCatalogProducts({page: 1, searcher: searcher}) setTableRows(data.products) } React.useEffect(() => { if(searcher) getData() }, [searcher]) return <CatalogTableView tableRows={tableRows} total={total} nextPage={nextPage} headers={headers} /> } export default CatalogTable
注意:如果你的Table组件需要接收实时的searcher参数,方案二会因为searcher变化触发视图层重渲染,这种场景优先选择方案一。
内容的提问来源于stack exchange,提问作者Evgeny Alekseev
相关产品推荐
相关产品推荐

