State management is a fundamental aspect of building modern web applications. As an application grows, managing and maintaining the states can become rather daunting. Redux, a popular JavaScript library, has been the go-to solution for state management in React applications for a while now.
However, Redux, or pure Redux to be specific, can be quite verbose and boilerplate-heavy. It requires a significantly lengthy setup, which is where Redux Toolkit comes in handy, offering a simplified and more efficient way to set up and manage state in your React applications.
What is Redux Toolkit?
Redux Toolkit is a much simpler way to set up and use Redux without the need to create large amounts of boilerplate.
Redux Persist
Redux helps applications hold onto data when navigating from page to page, which is known as the global state. However, as most people who are familiar with Redux will know, the global state will be reset the moment the application is reset.
Using Redux Toolkit with Redux Persist
The following tutorial will implement a simple login system, to fetch data to store it in Redux and into the localStorage (default) with Redux Persist. This way, routes can be restricted depending on the existence of data being present in the browser storage.
Custom hooks can do all of this, but the behavior may not always be consistent on all browsers, and the implementation can lead to UI inconsistencies. Redux Persist ensures that the app does not get loaded before the available data is fetched from the browser memory, and then lets the logic flow as it is implemented.
To follow along and understand this tutorial properly, here are some topics that you should have some idea about beforehand:
- React JS
- Redux
- Redux Toolkit
Setting up the React JS Project
First of all, the React project must be set up, and I'll use Vite to do this as it is easy to use. Run the command below:
> npm create vite@latest
You will be given a couple of prompts, such as the name of the project, which can be whatever you want. I named mine 'auth-redux'. Next, the prompt will be given to select the framework, I went with React and then when prompted for a variant, I selected JavaScript.
✔ Project name: auth-redux
✔ Select a framework: React
✔ Select a variant: JavaScript
After the setup is created, run the command:
> npm install
This will install the necessary node_modules
to get you started. Finally, to see the server go live, run the command:
> npm run dev
Installing Redux, Redux Toolkit and Redux Persist
With your server set up, it's time to install react-redux, redux-toolkit, and redux-persist, along with react-router-dom, which will be needed for navigating pages in React. We will also install Sass, to use .scss
files
> npm i react-router-dom react-redux @reduxjs/toolkit redux-persist sass
Once the setup is complete, we can proceed to create the project itself.
Creating the Login Module
For those who want to check every file and module used in the project, the code is available here for you to follow along with. There is also a TypeScript version in a separate branch for those who require it.
--src/
--assets/
--components/
--common/
--Navbar
--index.jsx
--index.scss
--pages/
--Home
--index.jsx
--index.scss
--Login
--index.jsx
--index.scss
--App.css
--App.jsx
--main.jsx
The project - two pages, a home page, and a login page.
The goal - to ensure that after logging in, the user is redirected to the home page. If the home page is attempted to be loaded manually in the browser by hitting the URL without logging in, it will redirect back to login. We will try to check and keep track of the login status using Redux and localStorage.
Creating a login form and a home page is fairly simple if you know React JS and CSS/SCSS. Here are the two pages I have implemented.
Log in:
Home:
The Redux and Redux Persist Setup
Once the pages and necessary components are set up, we can move to explaining the Redux logic. I will be implementing everything related to Redux inside a folder called store
.
First, we'll create a "slice" for the initial state of Redux. You can read more on creating Redux Slices here. Essentially, it will be an object, that will contain some properties as null.
const initialState = {
id: null,
username: null,
firstName: null,
lastName: null,
gender: null,
image: null,
token: null,
};
We can then use this to create our slice. We will create a couple of reducer functions to implement our logic for saving data when a user logs in, and removing data when a user logs out. This can be done with the syntax:
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
id: null,
username: null,
firstName: null,
lastName: null,
gender: null,
image: null,
token: null,
};
export const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
saveLogin: (state, action) => {
state.id = action.payload.id;
state.username = action.payload.username;
state.firstName = action.payload.firstName;
state.lastName = action.payload.lastName;
state.gender = action.payload.gender;
state.image = action.payload.image;
state.token = action.payload.token;
},
removeLogin: (state) => {
state.id = null;
state.username = null;
state.firstName = null;
state.lastName = null;
state.gender = null;
state.image = null;
state.token = null;
},
},
});
export const { saveLogin, removeLogin } = authSlice.actions;
export default authSlice.reducer;
With the slice created, we can now move on to create the store itself and the logic to persist the data. Now, we will create the universal reducer using combineReducers
function from redux toolkit.
import { combineReducers } from "@reduxjs/toolkit";
import authSlice from "./auth";
const reducers = combineReducers({
auth: authSlice,
});
Next, we have to set up the Redux Persist mechanism that will be able to retain the data using localStorage. We can do this by creating an object for the persist configuration, and it will require a key and a storage value.
const persistConfig = {
key: "root",
storage: localStorage,
whitelist: ["auth"],
};
Almost done, now using the reducers and persistConfig:
const persistedReducer = persistReducer(persistConfig, reducers);
Creating the store and the persistor objects to export them:
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [PERSIST],
},
}),
});
const persistor = persistStore(store);
export { store, persistor };
The persistor object is what will now be able to ensure that the initialState is created as soon as the app is loaded in the browser, and afterward, Redux Toolkit can handle the further functionalities.
In main.tsx
, which is the parent of App.jsx
, we will write the following:
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { persistor, store } from './store/index.js'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
</React.StrictMode>,
)
We are wrapping the App component in the PersistGate
to ensure that the App does not load until the data fetch/reception is completed between the application and the browser storage by passing the persistor object it that we created earlier. I have set the loading value to null, but you can use this to implement loading screens/components before data is properly loaded into Redux from storage.
And over that, we are wrapping the PersistGate
component in the Provider
component from Redux, passing the store as a prop so Redux Toolkit can handle the basic functionalities.
How the Private Route Logic Works
App.jsx
import "./App.css";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Login from "./components/pages/Login";
import Home from "./components/pages/Home";
import PrivateRoute from "./components/common/PrivateRoute";
import { useSelector } from "react-redux";
function App() {
const auth = useSelector((state) => state.auth);
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to={"/login"} />} />
<Route path="/login" element={!(auth.username && auth.id && auth.token) ? <Login /> : <Navigate to={"/page"} />} />
<Route element={<PrivateRoute />} >
<Route path="/page" element={<Home />} />
</Route>
</Routes>
</BrowserRouter>
)
}
export default App
PrivateRoute
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
const PrivateRoute = () => {
const auth = useSelector((state) => state.auth);
return auth.username && auth.id && auth.token ? <Outlet /> : <Navigate to={"/login"} />
};
export default PrivateRoute;
Here, the PrivateRoute component is going to check if the username, id, and token exist or not. Even if the app is reloaded, the PersistGate
along with the persistor
object will make sure that the data is fetched from the localStorage in the browser. It will then allow navigation to the <Outlet />
(Whatever component is supposed to be accessible), otherwise it will navigate back to the login page.
Redux Toolkit and Redux Persist at Work
Using the Inspect tab to monitor the data in the local storage, on first load, you can find that the data of the initial state is saved here in the login page as all properties set to null in the auth object.
After logging in, the data is updated accordingly to the response that is being sent from the login API. Every time you try to navigate to the login page now, you will be redirected back to login.
Conclusion
Redux can be fairly complex to work with, but Redux Toolkit makes it a lot easier when setting it up and creating the reducers. Redux Persist lets saving and fetching data into browser storage much more easily and conveniently and allows React applications to depend on browser storage when creating private routes to stop users from accessing certain features - even after hard reloading the application.
All of the source code for this project are available here.
Top comments (0)