Building an Admin Console with React Template, Redux, and Axios: How to Use Material-Dashboard-React (Part 1)
When building a CRM, ERP, or admin console, utilizing beautiful UI admin console templates can be an efficient way to create a responsive user experience. However, these templates often come with their own tech stacks that may not align with your preferred stacks. In this article, I will demonstrate how to modify the material-dashboard-react template (https://github.com/creativetimofficial/material-dashboard-react) to integrate Redux, Redux Toolkit, and Axios, making it suitable for real-world production applications.
Step 1: Replacing Internal Redux with react-redux
To incorporate Redux into the material-dashboard-react template, we need to replace the internal Redux with the react-redux library. Additionally, we’ll create a production-level Redux structure, including the store, slice, thunk, and selector. The internal Redux in material-dashboard-react is responsible for managing the UI structure, such as collapsing and expanding the left navigation panel. Since we have other modules that also require Redux for managing their status, particularly entities related to RESTful APIs, we’ll wrap the internal UI Redux into a Redux slice, which we’ll call ‘ui.slice.js’.
Refactored code in src/content/index.js to src/redux/ui/ui.slice.js:
import { createSlice } from "@reduxjs/toolkit";
export const initialUIState = {
miniSidenav: false,
transparentSidenav: false,
whiteSidenav: false,
sidenavColor: "info",
transparentNavbar: true,
fixedNavbar: true,
openConfigurator: false,
direction: "ltr",
layout: "dashboard",
darkMode: false,
};
export const uiSlice = createSlice({
name: "ui",
initialState: initialUIState,
reducers: {
setMiniSidenav: (state, action) => {
state.miniSidenav = action.payload;
},
setTransparentSidenav: (state, action) => {
state.transparentSidenav = action.payload;
},
setWhiteSidenav: (state, action) => {
state.whiteSidenav = action.payload;
},
setSidenavColor: (state, action) => {
state.sidenavColor = action.payload;
},
setTransparentNavbar: (state, action) => {
state.transparentNavbar = action.payload;
},
setFixedNavbar: (state, action) => {
state.fixedNavbar = action.payload;
},
setOpenConfigurator: (state, action) => {
state.openConfigurator = action.payload;
},
setDirection: (state, action) => {
state.direction = action.payload;
},
setLayout: (state, action) => {
state.layout = action.payload;
},
setDarkMode: (state, action) => {
state.darkMode = action.payload;
},
},
});
export const {
setMiniSidenav,
setTransparentSidenav,
setWhiteSidenav,
setSidenavColor,
setTransparentNavbar,
setFixedNavbar,
setOpenConfigurator,
setDirection,
setLayout,
setDarkMode,
} = uiSlice.actions;
export default uiSlice.reducer;
Step 2: Creating Redux Selector
To enhance code clarity and ease of use, we’ll create Redux selectors to replace the existing useMaterialUIController
hook with standard Redux selectors. This change allows front-end developers to better understand and utilize the code, while also facilitating unit testing.
Before refactored:
const [controller, dispatch] = useMaterialUIController();
const { miniSidenav } = controller;
Refactored code:
const dispatch = useDispatch();
const { miniSidenav } = useSelector(selectUI);
Step 3: Implementing RESTful API Entity-related Redux and Service Modules
To handle CRUD operations for entities in the front-end, we’ll create Redux and service modules. As an example, suppose we have an “account” table in our database. We’ll create an accountAPI.js file to handle API requests, along with account.slice.js, account.selector.js, and account.thunk.js files to connect the API.js and Redux.
Code in src/services/accountAPI.js:
import { get, post, put, del } from "./http";
import { buildApiUrl } from "./utils";
export const accountAPI = {
fetchAccounts: async (query) => {
const url = buildApiUrl("/accounts", query);
return await get(url);
},
fetchAccount: async (params) => {
const url = buildApiUrl("/accounts", params);
return await get(url);
},
createAccount: async (data) => {
const url = buildApiUrl("/accounts");
return await post(url, data);
},
updateAccount: async (params, data) => {
const url = buildApiUrl("/accounts", params);
return await put(url, data);
},
deleteAccount: async (params) => {
const url = buildApiUrl("/accounts", params);
return await del(url);
},
searchAccounts: async (query) => {
const url = buildApiUrl("/search/accounts");
return await post(url, query);
},
};
Code in src/redux/account/account.sclice.js
import { createSlice } from "@reduxjs/toolkit";
import { accountThunkReducers } from "./account.thunk";
export const initialAccountState = {
accounts: [],
account: null,
selectedAccountId: null,
status: "idle", // 'idle' | 'loading' | 'succeeded' | 'failed',
error: null,
};
export const accountSlice = createSlice({
name: "account",
initialState: initialAccountState,
reducers: {},
extraReducers: accountThunkReducers,
});
export default accountSlice.reducer;
Code in src/redux/account/account.selector.js
export const selectAccounts = (state) => (state.account ? state.account.accounts : []);
export const selectAccount = (state) => (state.account ? state.account.account : null);
export const selectAccountHttpStatus = (state) => (state.account ? state.account.status : null);
Code in src/redux/account/account.thunk.js
import { createAsyncThunk } from "@reduxjs/toolkit";
import { accountAPI } from "../../services/accountAPI";
export const createAccount = createAsyncThunk("account/createAccount", async (body, thunkAPI) => {
const rsp = await accountAPI.createAccount(body);
return rsp.data;
});
export const fetchAccounts = createAsyncThunk("account/fetchAccounts", async (query, thunkAPI) => {
const rsp = await accountAPI.fetchAccounts(query);
return rsp.data;
});
export const fetchAccount = createAsyncThunk("account/fetchAccount", async (_id, thunkAPI) => {
const rsp = await accountAPI.fetchAccount(_id);
return rsp.data;
});
export const searchAccounts = createAsyncThunk(
"account/searchAccounts",
async (query, thunkAPI) => {
const rsp = await accountAPI.searchAccounts(query);
return rsp.data;
}
);
export const updateAccount = createAsyncThunk(
"account/updateAccount",
async ({ _id, data }, thunkAPI) => {
const rsp = await accountAPI.updateAccount(_id, data);
return rsp.data;
}
);
export const deleteAccount = createAsyncThunk(
"account/deleteAccount",
async ({ _id }, thunkAPI) => {
const rsp = await accountAPI.deleteAccount(_id);
return rsp.data;
}
);
export const accountThunkReducers = (builder) => {
builder
.addCase(createAccount.pending, (state, action) => {
state.status = "loading";
})
.addCase(createAccount.fulfilled, (state, action) => {
state.status = "succeeded";
state.accounts.push(action.payload);
})
.addCase(createAccount.rejected, (state, action) => {
state.status = "failed";
state.error = action.error;
})
.addCase(fetchAccounts.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchAccounts.fulfilled, (state, action) => {
state.status = "succeeded";
state.accounts = action.payload;
})
.addCase(fetchAccounts.rejected, (state, action) => {
state.status = "failed";
state.error = action.error;
})
.addCase(fetchAccount.pending, (state, action) => {
state.status = "loading";
})
.addCase(fetchAccount.fulfilled, (state, action) => {
state.status = "succeeded";
state.account = action.payload;
})
.addCase(fetchAccount.rejected, (state, action) => {
state.status = "failed";
state.error = action.error;
})
.addCase(searchAccounts.pending, (state, action) => {
state.status = "loading";
})
.addCase(searchAccounts.fulfilled, (state, action) => {
state.status = "succeeded";
state.accounts = action.payload;
})
.addCase(searchAccounts.rejected, (state, action) => {
state.status = "failed";
state.error = action.error;
})
.addCase(updateAccount.pending, (state, action) => {
state.status = "loading";
})
.addCase(updateAccount.fulfilled, (state, action) => {
state.status = "succeeded";
const index = state.accounts.findIndex((it) => it._id === action.payload._id);
if (index !== -1) {
const item = state.accounts[index];
state.accounts[index] = { ...item, ...action.payload.data };
state.account = { ...item, ...action.payload.data };
}
})
.addCase(updateAccount.rejected, (state, action) => {
state.status = "failed";
state.error = action.error;
})
.addCase(deleteAccount.pending, (state, action) => {
state.status = "loading";
})
.addCase(deleteAccount.fulfilled, (state, action) => {
state.status = "succeeded";
state.accounts = state.accounts.filter((it) => it._id !== action.payload._id);
})
.addCase(deleteAccount.rejected, (state, action) => {
state.status = "failed";
state.error = action.error;
});
};
Step 4: Using Codeforge to Generate Boilerplate Code
Creating the aforementioned boilerplate code manually can be tedious and time-consuming. To streamline this process, I have developed a tool called Codeforge. You can find the tool on GitHub at https://github.com/zlkca/codeforge. Feel free to clone or fork the repository.
To define your template module using Codeforge, follow these steps:
- Open the
codeforge/template/module/index.js
file. - Export a
moduleMap
object that represents your desired module structure. For example:
export const moduleMap = {
account:{
name: 'account',
entities: [
{name: 'account'}
]
},
}
3. Save the file.
To generate the required services and Redux files, run the following command in your terminal:
node codeforge.js gen account --thunk
This command generates the necessary files for the specified module, including the Redux files with Redux Thunk support, service files, and other related components.
The generated files will be available in the ./dist
directory of your project. You can copy and modify them according to your needs.
Gernerating module: account ...
generate redux for redux-thunk ...
./dist/redux/account/account.thunk.js created!
./dist/redux/account/account.slice.js created!
./dist/redux/account/account.selector.js created!
./dist/redux/store.js created!
./dist/services/accountAPI.js created!
./dist/services/utils.js created!
./dist/services/http.js created!
./dist/components/account/AccountList.js created!
./dist/components/account/AccountForm.js created!The fourth step: install the axios, add your http.js file to hookup the axios:
Step 5: Installing Axios and Hooking Up http.js
Next, you’ll need to install Axios, a popular HTTP client library. Additionally, you need to add the http.js
file to connect Axios with your project.
Make sure you have Axios installed by running the following command:
npm i axios --save
Once Axios is installed, add the http.js
file to your project to configure and handle Axios requests. You can find an example of the http.js
file in the generated code from the previous step.
import axios from "axios"
const REQ_TIMEOUT = 1000 * 80 // 80 seconds
export const get = async (url) => {
const config = {
timeout: REQ_TIMEOUT,
}
try {
return await axios.get(url, config);
} catch (e) {
if(e.code === "ECONNABORTED"){ // server no response, Service Unavailable
return {data: [], status: 503};;
}else if(e.code === "ERR_NETWORK"){ // host is down, Gatway Timeout
return {data: [], status: 504};
}else{
const {data, status} = e.response;
return {data, status}
}
}
};
export const post = async (url, data) => {
const config = {
timeout: REQ_TIMEOUT,
}
try {
return await axios.post(url, data, config);
} catch (e) {
if(e.code === "ECONNABORTED"){ // server no response, Service Unavailable
return {data: null, status: 503};;
}else if(e.code === "ERR_NETWORK"){ // host is down, Gatway Timeout
return {data: null, status: 504};
}else{
const {data, status} = e.response;
return {data, status}
}
}
};
export const put = async (url, data) => {
const config = {
timeout: REQ_TIMEOUT,
}
try {
return await axios.put(url, data, config);
} catch (e) {
if(e.code === "ECONNABORTED"){ // server no response, Service Unavailable
return {data: null, status: 503};;
}else if(e.code === "ERR_NETWORK"){ // host is down, Gatway Timeout
return {data: null, status: 504};
}else{
const {data, status} = e.response;
return {data, status}
}
}
};
export const del = async (url) => {
const config = {
timeout: REQ_TIMEOUT,
}
try {
return await axios.delete(url, config);
} catch (e) {
if(e.code === "ECONNABORTED"){ // server no response, Service Unavailable
return {data: null, status: 503};;
}else if(e.code === "ERR_NETWORK"){ // host is down, Gatway Timeout
return {data: null, status: 504};
}else{
const {data, status} = e.response;
return {data, status}
}
}
};
Step 6: Modifying the store.js
File and Hooking Up in app.js
In this step, you’ll need to modify the store.js
file and integrate it into your app.js
file.
First, import the necessary dependencies in your store.js
file. For example:
import { configureStore } from "@reduxjs/toolkit";
import uiReducer from "../redux/ui/ui.slice";
import authReducer from "../redux/auth/auth.slice";
import roleReducer from "../redux/role/role.slice";
import accountReducer from "../redux/account/account.slice";
export default configureStore({
reducer: {
ui: uiReducer,
auth: authReducer,
role: roleReducer,
account: accountReducer,
},
});
Finally, in your app.js
file, import the required dependencies and wrap your App
component with the Provider
component from react-redux
. Here's an example:
import React from "react";
import { Provider } from "react-redux";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "App";
import store from "./redux/store";
const container = document.getElementById("app");
const root = createRoot(container);
root.render(
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
);
Step 7: Updating References to Redux Functions
In this step, you’ll need to update the references to the Redux functions in your codebase. For example, if you previously imported functions from the context
module, you'll need to update the imports to reference the relevant Redux files.
Here’s an example of how the imports might change:
import {
useMaterialUIController,
setMiniSidenav,
setTransparentSidenav,
setWhiteSidenav,
} from "context";
function Sidenav({ color, brand, brandName, routes, ...rest }) {
const [controller, dispatch] = useMaterialUIController();
const { miniSidenav, transparentSidenav, whiteSidenav, darkMode, sidenavColor } = controller;
...
}
to:
import { setMiniSidenav, setTransparentSidenav, setWhiteSidenav } from "../../redux/ui/ui.slice";
import { selectUI } from "redux/ui/ui.selector";
function Sidenav({ color, brand, brandName, routes, ...rest }) {
const dispatch = useDispatch();
const { miniSidenav, transparentSidenav, whiteSidenav, darkMode, sidenavColor } =
useSelector(selectUI);
...
}
Make sure to update all the references to the appropriate Redux functions throughout your codebase.
Conclusion and Next Steps
Congratulations! After completing the above steps, you should be able to run your application by executing:
npm run start
To observe the network request being sent out, you can insert the dispatch
statement in your files. For example, you can use dispatch(fetchAccounts())
to trigger the fetching of accounts. By doing so, you will be able to see the network request in your browser's debugger.
In the next article, I will guide you through the process of setting up authentication and creating a login page. This will enable users to securely access your application with proper authentication mechanisms.
Furthermore, we will cover another article that explains how to modify the list and form pages. This will allow you to perform CRUD (Create, Read, Update, Delete) operations directly from the user interface, providing a seamless experience for managing your data.