You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

重构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)
)

疑问与诉求

  1. 是否必须移除currentGuidelines这个Selector?如果需要移除,应该移到哪里,原因是什么?
  2. 如何让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
    />
  )
})

方案生效原理

  1. memo的拦截作用:原Guideline组件中,useSelector(currentGuidelines)会在任何一条Guideline变化时返回新的数组引用,导致组件触发重渲染。拆分后,Guideline组件的memo会对比自身props(如process),只有当前Guideline的自身数据变化时,才会触发重渲染。
  2. 缩小重渲染范围: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 11:02:06