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;
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;
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;
// 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);
};
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();
});
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;
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')
);
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]);
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.
Top comments (0)