Steve Blue
7 min readJul 7, 2023

The React app architecture, utilizing redux-injectors, redux-saga, redux-toolkit, and redux-persist, is a powerful combination for building scalable and maintainable applications. These libraries provide various functionalities and tools to enhance your Redux workflow and improve overall application performance.

Let’s break down each component and understand its role in the architecture:

  1. Redux: Redux is a state management library that allows you to manage application state in a predictable manner. It provides a central store to hold the state and follows a unidirectional data flow pattern. Redux is the foundation of this architecture.
  2. Redux-Saga: Redux-Saga is a middleware library that helps manage side effects, such as asynchronous actions, in your Redux application. It uses ES6 generators to handle complex async flows and provides a more structured way to manage asynchronous logic alongside your Redux actions and reducers.
  3. Redux-Toolkit: Redux-Toolkit is a set of utility functions and abstractions that simplifies common Redux tasks. It includes tools like createSlice for generating Redux slices, createAsyncThunk for handling async actions, and configureStore for creating the Redux store with sensible defaults. Redux-Toolkit helps reduce boilerplate code and improves developer productivity.
  4. Redux-Persist: Redux-Persist is a library that enables persisting Redux state across page reloads or app restarts. It provides an easy way to store and retrieve the Redux state from storage (e.g., localStorage, AsyncStorage). With Redux-Persist, you can ensure that user data and application state are preserved even after a refresh.
  5. Redux-Injectors: Redux-Injectors is a library that simplifies dynamic injection of reducers and sagas into the Redux store. It allows you to dynamically load and unload features or modules in your application, which is particularly useful for code splitting and lazy loading. Redux-Injectors enhances the modularity and scalability of your Redux architecture.

By combining these libraries, you can create a robust and efficient architecture for your React app. Redux provides the core state management, redux-saga handles asynchronous logic, redux-toolkit simplifies common Redux tasks, redux-persist ensures state persistence, and redux-injectors facilitates dynamic code loading.

To set up an application architecture using Redux, redux-injectors, redux-saga, redux-toolkit, and redux-persist, you can follow these general steps:

Step 1: Install the necessary dependencies Make sure you have the required packages installed in your project. You can use a package manager like npm or Yarn to install them. Here are the commands to install the dependencies:

npm install redux redux-saga redux-toolkit redux-persist redux-injectors

Step 2: Create a Redux store In your application’s entry point or a dedicated file for configuring Redux, set up your Redux store using redux-toolkit’s configureStore function. This function allows you to define reducers, middleware, and other store configuration options. Here's an example:

// modules
import { configureStore } from '@reduxjs/toolkit';
import { persistStore } from 'redux-persist';
import { createInjectorsEnhancer } from 'redux-injectors';

// root
import rootReducer from './reducers';
import rootSaga from './sagas';

const { run: runSaga } = sagaMiddleware;

const store = configureStore({
reducer: rootReducer,
middleware: [sagaMiddleware],
enhancers: [
createInjectorsEnhancer({
createReducer: rootReducer,
runSaga,
}),
],
});

runSaga(rootSaga);

const persistor = persistStore(store);

export { store, persistor };

In this example, rootReducer is a combined reducer that you need to define, and rootSaga is the root saga that combines all your sagas. Make sure you have the necessary reducers and sagas set up accordingly.

Step 3: Define your reducers and actions Use redux-toolkit’s createSlice function to define your reducers and actions in a concise manner. This function automatically generates action creators and action types based on the provided reducer functions. Here's an example:

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

const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment(state) {
return state + 1;
},
decrement(state) {
return state - 1;
},
},
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

Step 4: Create your sagas Write your sagas using redux-saga. Sagas are generator functions that listen for specific actions and perform asynchronous operations. Here’s an example:

// saga
import { put, takeEvery } from 'redux-saga/effects';

// reducers
import { increment } from '../reducers/counterSlice';

function* incrementAsync() {
yield new Promise((resolve) => setTimeout(resolve, 1000));
yield put(increment());
}

function* watchIncrementAsync() {
yield takeEvery('counter/incrementAsync', incrementAsync);
}

export default function* rootSaga() {
yield all([watchIncrementAsync()]);
}

In this example, incrementAsync is a saga that waits for 1 second using a promise and then dispatches the increment action using put. The watchIncrementAsync saga uses takeEvery to listen for the counter/incrementAsync action and run the incrementAsync saga.

Step 5: Configure redux-persist Configure redux-persist to persist your Redux state. You need to wrap your root component with PersistGate and provide the Redux store and persistor. Here's an example using React:

// modules
import { PersistGate } from 'redux-persist/integration/react';

// store
import { store, persistor } from './store';

const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
)
}

export default App

Step 6: Integrate redux-injectors With redux-injectors, you can dynamically inject reducers and sagas into your store. This allows you to modularize your code and add or remove features without modifying the core store setup. Consult the redux-injectors documentation for specific instructions on how to integrate it with your application.

Remember that these steps provide a general outline for setting up your application architecture using Redux, redux-injectors, redux-saga, redux-toolkit, and redux-persist. The actual implementation may vary based on your project structure and requirements.

To create custom hooks with redux-injectors, you can follow these steps:

Step 1: Set up redux-injectors Before creating custom hooks, make sure you have redux-injectors integrated into your Redux store setup as described in the previous responses.

Step 2: Define your custom hooks Create a new file, let’s say useInjectors.js, where you'll define your custom hooks. Inside this file, you can create hooks that utilize redux-injectors to dynamically inject reducers and sagas into the Redux store.

Here’s an example of a custom hook that injects a reducer:

import { useDispatch } from 'react-redux';
import { useInjectReducer } from 'redux-injectors';

const useInjectors = () => {
const dispatch = useDispatch();

// Inject a reducer dynamically into the Redux store
const injectReducer = (key, reducer) => {
useInjectReducer({ key, reducer }, { dispatch });
};

// Inject a saga dynamically into the Redux store
const injectSaga = (key, saga, options) => {
useInjectSaga({ key, saga }, { dispatch }, options);
};

return {
injectReducer,
injectSaga,
};
};

export default useInjectors;

Step 3: Use custom hooks in your components Once you have defined your custom hooks, you can use them in your components to dynamically inject reducers and sagas when needed.

// react
import React, { useEffect } from 'react';

// injectors
import useInjectors from './useInjectors';
import myReducer from './myReducer';

const MyComponent = () => {
const { injectReducer } = useInjectors();

useEffect(() => {
// Inject the reducer when the component mounts
injectReducer('myReducer', myReducer);
// Inject the saga when the component mounts
injectSaga('mySaga', mySaga);

return () => {
// Remove the reducer when the component unmounts (optional)
injectReducer('myReducer', null);
// Remove the saga when the component unmounts (optional)
injectSaga('mySaga', null);
};
}, [injectReducer]);

// Rest of the component code
// ...
};

export default MyComponent;

By using custom hooks like useInjectors, you can encapsulate the logic for injecting reducers and sagas, making it easier to reuse across your application and promoting a clean and scalable architecture.

Handling API requests

By following below steps, you can effectively handle API requests using Redux Saga within the given architecture. The Saga listens for the ‘FETCH_DATA’ action, makes the API call, and dispatches appropriate actions based on the success or failure of the request. The Redux slice handles the state management for the API request, including loading, error, and data. The redux-injectors library allows for dynamic injection of the Saga and reducer, enhancing the modularity and scalability of your application.

1. Create a Redux slice using redux-toolkit:

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

export const fetchData = createAsyncThunk('data/fetchData', async () => {
try {
const response = await fetch('https://api.example.com/data'); // Replace with your API endpoint
const data = await response.json();
return data;
} catch (error) {
throw new Error(error.message);
}
});

const dataSlice = createSlice({
name: 'data',
initialState: { loading: false, error: null, data: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchData.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchData.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchData.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
},
});

export default dataSlice.reducer;

2. Create a Saga for handling API requests:

// dataSaga.js

import { takeLatest, call, put } from 'redux-saga/effects';
import { fetchData } from '../reducers/dataSlice';

function* handleFetchData() {
try {
const response = yield call(fetchData); // Dispatch the fetchData async action
yield put(fetchData.fulfilled(response)); // Dispatch the success action with the response data
} catch (error) {
yield put(fetchData.rejected(error)); // Dispatch the failure action with the error
}
}

export default function* dataSaga() {
yield takeLatest(fetchData.pending.type, handleFetchData);
}

3. Inject the Saga and reducer using redux-injectors:

import { injectAsyncSaga, ejectAsyncSaga } from 'redux-injectors';

import dataSaga from './sagas/dataSaga';
import dataReducer from './reducers/dataSlice';

injectAsyncSaga('data', dataSaga); // Inject the 'dataSaga' Saga with a unique key
injectAsyncReducer('data', dataReducer); // Inject the 'dataReducer' with the same key as the Saga

4. Dispatch the API request action from your component:


import React, { useEffect } from 'react';

import { useDispatch, useSelector } from 'react-redux';

import { fetchData } from './reducers/dataSlice';

const MyComponent = () => {
const dispatch = useDispatch();
const { loading, error, data } = useSelector((state) => state.data);

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

if (loading) {
return <Text>Loading...</Text>;
}

if (error) {
return <Text>Error: {error}</Text>;
}

return <Text>Data: {JSON.stringify(data)}</Text>;
};

export default MyComponent;

Utilizing redux-injectors, redux-saga, redux-toolkit, and redux-persist, provides a solid foundation for building scalable and maintainable React applications. Overall, it is a well-designed architecture that incorporates popular libraries and best practices for state management, asynchronous operations, code organization, and state persistence. It can be considered intermediate to advanced. While the individual libraries are well-documented and have a supportive community, properly integrating them and understanding their nuances may require a good understanding of Redux concepts, asynchronous programming, and React application architecture.

A senior-level developer would likely be comfortable with this architecture and possess the experience and knowledge to optimize performance, handle more complex scenarios, and make informed decisions regarding code structure, scalability, and maintainability.

Steve Blue
Steve Blue

Written by Steve Blue

Experienced Mobile Application Developer with a demonstrated history of working in the computer software industry.

Responses (1)