Centralized Loading State Management (Advanced) in react using redux toolkit

Arshan Nawaz - Aug 17 - - Dev Community

Setting up a Redux store with custom middleware and managing loading states across your app can be straightforward with a well-structured folder setup. Below is a step-by-step guide to organizing your Redux logic in a scalable and maintainable way.

1. src/redux/: Application Setup
**
store.js:** This file is where you configure your Redux store, apply the middleware, and combine reducers.

// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
import loadingMiddleware from '../middleware/loading.middleware.js';

const store = configureStore({
  reducer: persistedReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false, // Disable serializable checks for redux-persist
    }).concat(loadingMiddleware),
});

export default store;
Enter fullscreen mode Exit fullscreen mode

rootReducer.js: Combines all the slices into a single root reducer.

import { combineReducers } from '@reduxjs/toolkit';
import loadingReducer from '../features/loading/loading.slice.js';
import dataAReducer from '../features/dataA/dataASlice';
import dataBReducer from '../features/dataB/dataBSlice';

const rootReducer = combineReducers({
  loading: loadingReducer,
  dataA: dataAReducer,
  dataB: dataBReducer,
});

export default rootReducer;
Enter fullscreen mode Exit fullscreen mode

2. src/redux/features/: Feature-Specific Logic
Each feature of your application gets its own directory inside the features folder. This keeps your code modular and easier to maintain.

loading/: Handles loading states across the app.

loading.slice.js: Contains the slice for managing loading states.
loading.middleware.js: The middleware responsible for managing loading states based on async actions.

import { createSlice } from '@reduxjs/toolkit';

const initialState = {};

const loadingSlice = createSlice({
  name: 'loading',
  initialState,
  reducers: {
    setLoading: (state, action) => {
      const { key, value } = action.payload;
      state[key] = value;
    },
  },
});

export const { setLoading } = loadingSlice.actions;
export default loadingSlice.reducer;
Enter fullscreen mode Exit fullscreen mode
// src/redux/middleware/loading.middleware.js
import { setLoading } from '../features/loading/loading.slice';

export const loadingMiddleware = (store) => (next) => (action) => {
  const { dispatch } = store;

  if (action.type.endsWith('/pending')) {
    dispatch(setLoading({ key: action.type.replace('/pending', ''), value: true }));
  } else if (action.type.endsWith('/fulfilled') || action.type.endsWith('/rejected')) {
    dispatch(setLoading({ key: action.type.replace(/\/(fulfilled|rejected)$/, ''), value: false }));
  }

  return next(action);
};
Enter fullscreen mode Exit fullscreen mode

3. Example service/function

// dataAThunks.js
import { createAsyncThunk } from '@reduxjs/toolkit';

export const fetchDataA = createAsyncThunk('data/fetchDataA', async () => {
  const response = await fetch('/api/dataA');
  return response.json();
});
Enter fullscreen mode Exit fullscreen mode

4. src/components/: UI Components
MyComponent.js: This component fetches and displays data from both fetchDataA and fetchDataB, using the loading states managed by the middleware.
fetchDataA and fetchDataB are the example functions for slices in async thunk or in redux

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchDataA } from '../features/dataA/dataAThunks';
import { fetchDataB } from '../features/dataB/dataBThunks';

const MyComponent = () => {
  const dispatch = useDispatch();
  const isLoadingFetchDataA = useSelector((state) => state.loading['data/fetchDataA']);
  const isLoadingFetchDataB = useSelector((state) => state.loading['data/fetchDataB']);

  useEffect(() => {
    dispatch(fetchDataA());
    dispatch(fetchDataB());
  }, [dispatch]);

  return (
    <div>
      {isLoadingFetchDataA ? (
        <p>Loading Data A...</p>
      ) : (
        <p>Data A Loaded!</p>
      )}

      {isLoadingFetchDataB ? (
        <p>Loading Data B...</p>
      ) : (
        <p>Data B Loaded!</p>
      )}
    </div>
  );
};

export default MyComponent;
Enter fullscreen mode Exit fullscreen mode

5. src/index.js: Application Entry Point
The index.js file initializes your React application and provides the Redux store to the app.

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './app/store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

6. (Optional)

you can me actionTypes to avoid spelling mistake etc when getting loading state of any service/function

// src/redux/actions/actionTypes.js

export const FetchDataA = 'data/fetchDataA';
export const FetchDataB = 'data/fetchDataB';

// And import in any component 
import { FetchDataA } from "../../../redux/actions/actionTypes";

 const isLoadinggetProjects = useSelector((state) => state.loading[FetchDataA]);
Enter fullscreen mode Exit fullscreen mode

Summary:
This folder structure helps you keep your Redux logic, especially with multiple async actions and custom middleware, organized and maintainable. Each feature (like dataA and dataB) has its own slice and thunk files, and global state management tasks like loading states are centralized in a dedicated folder. This separation of concerns ensures that your application remains scalable as it grows.

. . . . . . . . .
Terabox Video Player