javascriptreactjsreduxreact-context

Migrate React Context to Redux


I was using a free React template which was implemented with Context+Reducer and soon, after adding complexity, I ran accross the classic re-render caveat of useContext API. Is it possible to change my Context interface to one of Redux, since I don't want to re-render everything each time a single action is dispatched. I am currently using React v17.0.2


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,
};

I was trying to update fileData state and I ran across a breaking issue involving infinite re-renders, aswell as some non breaking issues regarding the rendering speed and processing! Any help is much appreciated


Solution

  • its pretty straightforward. After you have installed redux and redux toolkit. You can refactor your state setup sth like this:

    import { createSlice, configureStore } from '@reduxjs/toolkit';
    import { Provider } from 'react-redux';
    
    // define initial state
    const initialState = {
      miniSidenav: false,
      chart: {},
      chartIds: ["Main"],
      fileData: [],
      analysisData: [],
      isUploaded: false,
      layout: "page",
      darkMode: false,
    };
    
    // define the slice
    const materialSlice = createSlice({
      name: 'material',
      initialState,
      reducers: {
        setMiniSidenav: (state, action) => {
          state.miniSidenav = action.payload;
        },
        resetIds: (state) => {
          state.chartIds = ["Main"];
        },
        addNewChart: (state, action) => {
          state.chart = action.payload.value;
          state.chartIds = [...state.chartIds, 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,
    } = materialSlice.actions;
    
    // configure store
    const store = configureStore({
      reducer: materialSlice.reducer,
    });
    
    // context provider
    function MaterialUIControllerProvider({ children }) {
      return <Provider store={store}>{children}</Provider>;
    }
    
    export { MaterialUIControllerProvider };
    

    then you can use useDispatch and useSelector in your components to select a state and change them accordingly. As for infinite loop you might wanna check you are actually dispatching the action. Maybe it has something to do with dependencies inside your useEffect?