将React Context迁移至Redux:解决重渲染及无限渲染问题
问题描述
使用基于Context+Reducer实现的免费React模板,增加项目复杂度后出现以下问题:
- useContext API的全局重渲染问题:触发单个action时所有消费Context的组件都会重渲染
- 更新
fileData状态时出现无限重渲染循环 - 整体渲染速度与数据处理性能不足
相关代码:
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无限重渲染问题
问题根源
- 直接在组件渲染周期内调用
setFileData,导致状态更新后触发组件重渲染,重渲染又再次调用setFileData,形成循环 - 当前Context的
value缓存逻辑无效:useMemo依赖controller(每次状态更新都会生成新对象),导致所有消费Context的组件都会触发重渲染
修复步骤
- 限制
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]); }
- 优化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> ); }
三、额外性能优化建议
- 使用
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);
- 用
React.memo包裹纯组件:对于接收props的纯组件,使用React.memo避免不必要的重渲染
import React from 'react'; const ChartComponent = React.memo(({ chartData }) => { // 组件内容 });
- 大数据处理优化:如果
fileData是大型数据集,考虑使用Web Workers处理数据,避免阻塞主线程影响渲染速度;或采用分片加载策略,逐步渲染数据。
内容的提问来源于stack exchange,提问作者Giorgos Boletos
相关产品推荐
相关产品推荐

