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

将React Context迁移至Redux:解决重渲染及无限渲染问题

问题描述

使用基于Context+Reducer实现的免费React模板,增加项目复杂度后出现以下问题:

  1. useContext API的全局重渲染问题:触发单个action时所有消费Context的组件都会重渲染
  2. 更新fileData状态时出现无限重渲染循环
  3. 整体渲染速度与数据处理性能不足

相关代码:

import { createContext, useContext, useReducer, useMemo } from "react";

// prop-types is a library for typechecking of props
import PropTypes from "prop-types";

//React main context
const MaterialUI = createContext();

// Setting custom name for the context which is visible on react dev tools
MaterialUI.displayName = "MaterialUIContext";

//React reducer
function reducer(state, action) {
  switch (action.type) {
    case "MINI_SIDENAV": {
      return { ...state, miniSidenav: action.value };
    }
    case "RESET_IDS": {
      return { ...state, chartIds: ["Main"] };
    }
    case "ADD_CHART": {
      return { ...state, chart: action.value, chartIds: [...state.chartIds, action.id] };
    }
    case "DELETE_CHART": {
      return {
        ...state,
        chart: action.value,
        chartIds: state.chartIds.filter((id) => id !== action.id),
      };
    }
    case "FILE_DATA": {
      return { ...state, fileData: action.value };
    }
    case "ANALYSIS_DATA": {
      return { ...state, analysisData: action.value };
    }
    case "DATA_UPLOADED": {
      return { ...state, isUploaded: action.value };
    }
    case "LAYOUT": {
      return { ...state, layout: action.value };
    }
    default: {
      throw new Error(`Unhandled action type: ${action.type}`);
    }
  }
}

// React context provider
function MaterialUIControllerProvider({ children }) {
  const initialState = {
    miniSidenav: false,
    chart: {},
    chartIds: ["Main"],
    fileData: [],
    analysisData: [],
    isUploaded: false,
    layout: "page",
    darkMode: false,
  };

  const [controller, dispatch] = useReducer(reducer, initialState);

  const value = useMemo(() => [controller, dispatch], [controller, dispatch]);

  return <MaterialUI.Provider value={value}>{children}</MaterialUI.Provider>;
}

//React custom hook for using context
function useMaterialUIController() {
  const context = useContext(MaterialUI);

  if (!context) {
    throw new Error(
      "useMaterialUIController should be used inside the MaterialUIControllerProvider."
    );
  }

  return context;
}

// Typechecking props for the MaterialUIControllerProvider
MaterialUIControllerProvider.propTypes = {
  children: PropTypes.node.isRequired,
};

// Context module functions
const setMiniSidenav = (dispatch, value) => dispatch({ type: "MINI_SIDENAV", value });
const resetIds = (dispatch) => dispatch({ type: "RESET_IDS" });
const addNewChart = (dispatch, value, id) => dispatch({ type: "ADD_CHART", value, id });
const deleteChart = (dispatch, value, id) => dispatch({ type: "DELETE_CHART", value, id });
const setFileData = (dispatch, value) => dispatch({ type: "FILE_DATA", value });
const setAnalysisData = (dispatch, value) => dispatch({ type: "ANALYSIS_DATA", value });
const setIsUploaded = (dispatch, value) => dispatch({ type: "DATA_UPLOADED", value });
const setLayout = (dispatch, value) => dispatch({ type: "LAYOUT", value });

export {
  MaterialUIControllerProvider,
  useMaterialUIController,
  setMiniSidenav,
  resetIds,
  addNewChart,
  deleteChart,
  setFileData,
  setAnalysisData,
  setIsUploaded,
  setLayout,
};

解决方案

一、先解决fileData无限重渲染问题

问题根源

  1. 直接在组件渲染周期内调用setFileData,导致状态更新后触发组件重渲染,重渲染又再次调用setFileData,形成循环
  2. 当前Context的value缓存逻辑无效:useMemo依赖controller(每次状态更新都会生成新对象),导致所有消费Context的组件都会触发重渲染

修复步骤

  1. 限制setFileData的触发时机:用useEffect包裹调用逻辑,仅当数据真正变化时才触发dispatch
// 示例组件中的调用方式
import { useMaterialUIController } from './path-to-context';
import { useEffect } from 'react';

// 自定义浅比较函数,避免不必要的dispatch
const arraysEqual = (a, b) => {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (JSON.stringify(a[i]) !== JSON.stringify(b[i])) return false;
  }
  return true;
};

function FileUploadComponent() {
  const [controller, dispatch] = useMaterialUIController();
  const newFileData = []; // 你的新数据来源

  useEffect(() => {
    if (!arraysEqual(controller.fileData, newFileData)) {
      setFileData(dispatch, newFileData);
    }
  }, [controller.fileData, newFileData, dispatch]);
}
  1. 优化Context缓存(临时方案):将dispatch单独拆分,利用其稳定性(useReducer返回的dispatch不会随状态变化)减少不必要的重渲染
// 修改MaterialUIControllerProvider
function MaterialUIControllerProvider({ children }) {
  const initialState = { /* ... */ };
  const [controller, dispatch] = useReducer(reducer, initialState);

  // 拆分controller和dispatch为两个独立的Context(可选)
  const controllerValue = useMemo(() => controller, [controller]);
  const dispatchValue = useMemo(() => dispatch, []);

  return (
    <MaterialUI.Provider value={{ controller: controllerValue, dispatch: dispatchValue }}>
      {children}
    </MaterialUI.Provider>
  );
}

二、迁移到Redux(彻底解决全局重渲染问题)

推荐使用Redux Toolkit(官方标准工具集),它简化了Redux开发流程,自带不可变更新处理,且通过useSelector支持精确状态订阅,从根源避免全局重渲染。

步骤1:安装依赖

npm install @reduxjs/toolkit react-redux

步骤2:创建Redux Slice

将原有的Reducer逻辑转换为Redux Toolkit的Slice:

// src/store/slices/materialUISlice.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  miniSidenav: false,
  chart: {},
  chartIds: ["Main"],
  fileData: [],
  analysisData: [],
  isUploaded: false,
  layout: "page",
  darkMode: false,
};

const materialUISlice = createSlice({
  name: 'materialUI',
  initialState,
  reducers: {
    setMiniSidenav: (state, action) => {
      state.miniSidenav = action.payload;
    },
    resetIds: (state) => {
      state.chartIds = ["Main"];
    },
    addNewChart: (state, action) => {
      state.chart = action.payload.value;
      state.chartIds.push(action.payload.id);
    },
    deleteChart: (state, action) => {
      state.chart = action.payload.value;
      state.chartIds = state.chartIds.filter(id => id !== action.payload.id);
    },
    setFileData: (state, action) => {
      state.fileData = action.payload;
    },
    setAnalysisData: (state, action) => {
      state.analysisData = action.payload;
    },
    setIsUploaded: (state, action) => {
      state.isUploaded = action.payload;
    },
    setLayout: (state, action) => {
      state.layout = action.payload;
    },
  },
});

export const {
  setMiniSidenav,
  resetIds,
  addNewChart,
  deleteChart,
  setFileData,
  setAnalysisData,
  setIsUploaded,
  setLayout,
} = materialUISlice.actions;

export default materialUISlice.reducer;

步骤3:创建Redux Store

// src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import materialUIReducer from './slices/materialUISlice';

export const store = configureStore({
  reducer: {
    materialUI: materialUIReducer,
  },
});

步骤4:在根组件包裹Provider

// src/App.js
import { Provider } from 'react-redux';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      {/* 你的应用组件树 */}
    </Provider>
  );
}

export default App;

步骤5:在组件中使用Redux

替换原有的useMaterialUIController,使用useSelector精确订阅所需状态,useDispatch触发action:

import { useSelector, useDispatch } from 'react-redux';
import { setFileData, setMiniSidenav } from '../store/slices/materialUISlice';

function ExampleComponent() {
  // 仅订阅组件需要的状态,只有该状态变化时才会重渲染
  const fileData = useSelector(state => state.materialUI.fileData);
  const miniSidenav = useSelector(state => state.materialUI.miniSidenav);
  const dispatch = useDispatch();

  const handleUpdateFileData = (data) => {
    dispatch(setFileData(data));
  };

  return (
    <div>
      {/* 组件内容 */}
    </div>
  );
}

三、额外性能优化建议

  1. 使用createSelector缓存计算结果:对于复杂的状态派生逻辑,用reselect(Redux Toolkit已内置)缓存计算结果,避免每次渲染重复计算
import { createSelector } from '@reduxjs/toolkit';

const selectMaterialUI = state => state.materialUI;
export const selectActiveChartIds = createSelector(
  [selectMaterialUI],
  (materialUI) => materialUI.chartIds.filter(id => id !== 'Main')
);

// 组件中使用
const activeChartIds = useSelector(selectActiveChartIds);
  1. 用React.memo包裹纯组件:对于接收props的纯组件,使用React.memo避免不必要的重渲染
import React from 'react';

const ChartComponent = React.memo(({ chartData }) => {
  // 组件内容
});
  1. 大数据处理优化:如果fileData是大型数据集,考虑使用Web Workers处理数据,避免阻塞主线程影响渲染速度;或采用分片加载策略,逐步渲染数据。

内容的提问来源于stack exchange,提问作者Giorgos Boletos

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 03:54:51