重构Selector避免多属性DTO状态引发的组件重复渲染
问题背景
需要使用ProcedureParameterList组件渲染Guideline列表,其中ProcedureParameterList和ProcedureParameter是适配DTO的通用组件,无法修改。
问题现象
修改单条Guideline的文本时,整个列表会触发重渲染,而非仅修改的那一个Guideline组件。通过DevTools排查发现:
ProcedureParameterList因props变化触发重渲染- 底层Guideline组件因**useSelector获取
allGuidelines的逻辑(Hook 3)**变化重渲染
移除const allGuidelines: GuidelineDTO[] = useSelector(currentGuidelines)后,仅修改的组件会重渲染。推测原因是currentGuidelines监听整个Guideline数组,任何一条数据变化都会让Selector重新计算,导致所有Guideline组件感知到状态变化。
相关代码
export type GuidelineDTO = { text: string mainGuideline: number other: string } export type SomeProps<T extends GuidelineDTO> = { index: number expanded: boolean onExpandedChange: (expanded: boolean) => void onTextChange?: (bezeichnung: string) => void children?: React.ReactNode draggable?: boolean process: T } export const currentPoolSelector = createSelector( [(state: RootState) => state.pool.currentPoolDTO], (currentPoolDTO) => currentPoolDTO ) export const currentGuidelinesSelector = createSelector( [currentPoolSelector], (pool) => pool.guidelineList ) export const currentGuidelines = createSelector( [currentGuidelinesSelector], (guidelineList) => { return myfunc(guidelineList.guidelines) } ) const Guideline = memo((props: SomeProps<GuidelineDTO>): JSX.Element =>{ const {process: guideline} = props const allGuidelines: GuidelineDTO[] = useSelector(currentGuidelines) const dispatch = useAppDispatch() const [mainGuidelines, setMainGuidelines] = useState<GuidelineDTO[]>() useEffect(() => { setMainGuidelines(allGuidelines.filter(g => guideline.mainGuideline)) }, [allGuidelines, guideline.mainGuideline]); const mainGuideine: GuidelineDTO = useMemo( ()=> guideline.mainGuideline == null? null : mainGuidelines.filter(g => g.mainGuideline !== guideline.mainGuideline)[0] ,[ mainGuidelines, guideline.mainGuideline]) const handleMainGuidelineChange = useCallback( (neuG: Value<GuidelineDTO>): void => { const mainGuidelinenumber: number = (neuG as GuidelineDTO)?.mainGuideline dispatch(setPoolGuidelineNumber({ guideline, mainGuidelinenumber })) }, [dispatch, guideline] ) const dispatchTextChanged = useCallback( (text: string) => { dispatch(setpoolGuidelineText({guideline, text })) }, [dispatch, guideline] ) return ( <> <ProcedureParameter key={`processparameter${index}-${guideline?.nummer}`} onTextChange={dispatchTextChanged} {...props} > {mainGuideineEnabled && ( <Select options={mainGuidelines ?? []} onChange={handleMainGuidelineChange} clearable usePortal /> )} </ProcedureParameter> </> ) }) const GuidelinesList = (): JSX.Element => { const guidelinesList= useSelector(currentGuidelinesSelector) return ( <ProcedureParameterList key='overview-guidelines' context='guidelines' procedureParameterList={guidelinesList.guidelines ?? []} procedureParameterComponent={Guideline} /> ) } export default GuidelinesList
尝试过的方案
- 尝试直接传入当前Guideline,未实现预期的局部重渲染效果
- 同事提供以下方案,但无法理解原理:
//selectors.ts export const currentGuidelineTry= ( currentGuidline ) => createSelector( [currentGuidelines], (guidlinesList) => { return myfunc(guidelineList.guidelines) } ) //Guideline.tsx const allGuidelines = currentGuidelineTry(guideline) const selectableMainGuidelines = useSelector( state => allGuidelines(state) )
疑问与诉求
- 是否必须移除
currentGuidelines这个Selector?如果需要移除,应该移到哪里,原因是什么? - 如何让Selector仅跟踪与当前组件相关的状态?比如修改编号2的Guideline时,编号5的Guideline完全不需要感知这个变化。
已解决但原理不明
通过拆分组件解决了问题:将allGuidelines作为props传递给新拆分的MainGuideline组件后,仅修改的Guideline组件会重渲染。重构后代码如下:
const Guideline = memo((props: SomeProps<GuidelineDTO>): JSX.Element => { const {process: guideline} = props const allGuidelines: GuidelineDTO[] = useSelector(currentGuidelines) const dispatch = useAppDispatch() return ( <> <ProcedureParameter key={`processparameter${index}-${guideline?.nummer}`} onTextChange={dispatchTextChanged} {...props} > {mainGuideineEnabled && <MainGuideline allGuidelines={allGuidelines}/> } </ProcedureParameter> </> ) }) interface MainGuidelineProps { allGuidelines?: GuidelineDTO[] } const MainGuideline = memo((props: MainGuidelineProps) => { const {allGuidelines} = props const [mainGuidelines, setMainGuidelines] = useState<GuidelineDTO[]>() useEffect(() => { setMainGuidelines(allGuidelines.filter(g => guideline.mainGuideline)) }, [allGuidelines, guideline.mainGuideline]); const mainGuideine: GuidelineDTO = useMemo( () => guideline.mainGuideline == null ? null : mainGuidelines.filter(g => g.mainGuideline !== guideline.mainGuideline)[0] , [mainGuidelines, guideline.mainGuideline]) const handleMainGuidelineChange = useCallback( (neuG: Value<GuidelineDTO>): void => { const mainGuidelinenumber: number = (neuG as GuidelineDTO)?.mainGuideline dispatch(setPoolGuidelineNumber({guideline, mainGuidelinenumber})) }, [dispatch, guideline] ) const dispatchTextChanged = useCallback( (text: string) => { dispatch(setpoolGuidelineText({guideline, text})) }, [dispatch, guideline] ) return ( <Select options={mainGuidelines ?? []} onChange={handleMainGuidelineChange} clearable usePortal /> ) })
方案生效原理
- memo的拦截作用:原
Guideline组件中,useSelector(currentGuidelines)会在任何一条Guideline变化时返回新的数组引用,导致组件触发重渲染。拆分后,Guideline组件的memo会对比自身props(如process),只有当前Guideline的自身数据变化时,才会触发重渲染。 - 缩小重渲染范围:
MainGuideline作为子组件,仅当传入的allGuidelines或自身依赖的guideline属性变化时才会更新。但因为Guideline组件只有修改的那条才会重渲染,所以对应的MainGuideline才会触发更新,其他Guideline组件因props未变化被memo拦截,不会触发子组件更新。
更优的Selector优化方案
1. 参数化专属Selector
为每个Guideline组件创建独立的Selector,只监听与自身相关的状态:
// selectors.ts export const makeSelectMainGuidelinesForCurrent = () => { return createSelector( [currentGuidelines, (state, ownGuideline: GuidelineDTO) => ownGuideline], (allGuidelines, ownGuideline) => { // 仅返回当前Guideline需要的过滤后列表 return allGuidelines.filter(g => g.mainGuideline === ownGuideline.mainGuideline); } ); }; // Guideline组件内 const selectMainGuidelines = useMemo(() => makeSelectMainGuidelinesForCurrent(), []); const mainGuidelines = useSelector(state => selectMainGuidelines(state, guideline));
原理:每个Guideline组件拥有独立的Selector实例,只有当allGuidelines中与当前组件相关的内容变化时,才会重新计算返回值,避免无关变化触发重渲染。
2. 优化Selector返回值稳定性
确保currentGuidelines在内容未变化时返回相同的数组引用:
export const currentGuidelines = createSelector( [currentGuidelinesSelector], (guidelineList) => { const result = myfunc(guidelineList.guidelines); // reselect默认会缓存前一次输入输出,仅当输入变化时重新计算 // 需确保reducer中更新guidelineList.guidelines时,仅在内容真的变化时创建新数组 return result; } );
注意:Redux reducer中更新数组时,要遵循不可变原则,但不要在内容未变时创建新数组引用,否则会导致Selector无意义地重新计算。
3. 父组件统一管理全局状态
在GuidelinesList中统一获取allGuidelines,通过props传递给子组件,避免每个子组件单独调用useSelector:
const GuidelinesList = (): JSX.Element => { const guidelinesList= useSelector(currentGuidelinesSelector); const allGuidelines = useSelector(currentGuidelines); // 父组件统一获取 return ( <ProcedureParameterList key='overview-guidelines' context='guidelines' procedureParameterList={guidelinesList.guidelines ?? []} procedureParameterComponent={(props) => <Guideline {...props} allGuidelines={allGuidelines} />} /> ) }; // 修改Guideline组件props,移除内部useSelector const Guideline = memo((props: SomeProps<GuidelineDTO> & {allGuidelines: GuidelineDTO[]}): JSX.Element =>{ const {process: guideline, allGuidelines} = props; // 后续逻辑不变 });
原理:父组件统一获取全局状态后,Guideline组件的memo会对比allGuidelines引用和自身process props,只有当当前Guideline自身数据变化时才会重渲染,进一步缩小更新范围。
内容的提问来源于stack exchange,提问作者physicsuser

