What is Redux?
As the official documentation states: Redux is a pattern and library for managing and updating global application state. It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion (without causing side effects or unexpected behaviors).
So Redux works as your application’s global state manager, ensuring that every change in the state goes through a specific and organized process.
Think of the state as a JavaScript object that needs to be shared across all of your app’s components.
If you have a user object like this:
const user = {
username: "Jane Doe",
email: "email@example.com",
};
Sharing this object with every component can quickly get messy if you manually pass it down through your app’s component tree. This is where Redux comes in handy.
How Redux Works
Redux has its own set of terms and rules that each component should follow to access or update the data. The most basic ones are actions
and dispatch
.
An action in Redux describes what a component wants to do with the state. For example:
- Does it want to update the user’s username?
- Change their email?
- Clear the user’s data when they log out and that data isn't needed anymore?
These tasks can be transformed into action
types that Redux can understand and process.
Defining Actions
Actions are simple JavaScript objects that contain a type (a string that identifies the action) and optionally a payload (data to update the state).
Here are some example actions:
const emailUpdated = {
type: "user/emailUpdated",
payload: "email@main.co",
};
const usernameUpdated = {
type: "user/usernameUpdated",
payload: "Hajar",
};
const clearUserData = {
type: "user/clearUserData",
};
With these defined, components can use them to specify what kind of changes they want to make to the state.
Now the components know which actions
they need to perform and what data/payload to include with them, so they need to dispatch
them to the Redux store.
The Dispatch Function: Sending Actions to Redux
The dispatch
function is how actions
are sent to the Redux store. It’s a regular JavaScript function, the only differences are:
- Redux knows exactly what the
dispatch
function is and what to do when it's called within a component. - The function accepts action objects, which must include a type (a string that identifies the action).
When you call dispatch
with an action
, Redux processes it and then triggers the right reducer
that handles it.
What Are Reducers?
A reducer
is a Redux function that takes the current state
and an action
, determines how to update the state
based on the action
type, and then returns the updated one.
For example, if a reducer receives an action like user/usernameUpdated
, it modifies the username in the state with the value from the action’s payload. Once the reducer updates the state, Redux notifies the relevant components to re-render.
Et voilà! The component now has the updated user data, without having to worry about the steps Redux took to make it happen.
An example of how a reducer
could look:
const ACTION_TYPES = {
UPDATE_USERNAME: "user/usernameUpdated",
UPDATE_EMAIL: "user/emailUpdated",
CLEAR_STATE: "user/userDataCleared",
};
const initialState = {
username: "",
email: "",
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPES.UPDATE_USERNAME:
return {
// spread the state to return a new state object
...state,
username: action.payload,
};
case ACTION_TYPES.UPDATE_EMAIL:
return {
...state,
email: action.payload,
};
case ACTION_TYPES.CLEAR_STATE:
return initialState;
default:
return state;
}
};
export default userReducer;
And this is how the component would dispatch
the actions
:
const updateUsername = (name) => {
dispatch({
type: "user/usernameUpdated",
payload: name,
});
};
const updateEmail = (email) => {
dispatch({
type: "user/emailUpdated",
payload: email,
});
};
const clearUserData = () => {
dispatch({
type: "user/userDataCleared",
});
};
That's cool and all but have you noticed how much code we've written for such a simple reducer example?
- We had to define all the actions and make sure they were properly used in the components.
- We had to create a switch case block for each action.
- We had to remember to spread the previous state before updating its
email
orusername
. (this is to copy the old state to a new object first and return the new one -- reducers need to be pure functions).
So you can imagine how much more we'd need to write to introduce reducers
and actions
in a real-world application. This is where Redux ToolKit comes in very handy.
Using Redux Toolkit with Next.js
To use Redux Toolkit in an Next.js project, we need to install Next.js:
npx create-next-app@latest
Then, install @reduxjs/toolkit
and react-redux
:
npm install @reduxjs/toolkit react-redux
Configuring the Redux Store
Next, create a file app/redux/store.ts
to configure the Redux store. we'll use the configureStore
API from Redux Toolkit.
import { configureStore } from '@reduxjs/toolkit';
export default configureStore({
// The reducer will be added here later
reducer: {}
});
Wrapping the Application with Redux Provider
To make your Redux store accessible throughout your application, wrap your root component with the Provider
component from react-redux
and pass the store
to it.
"use client";
import { Provider } from "react-redux";
import store from "@/app/redux/store";
import MainContent from "@/app/components/MainContent";
export default function Home() {
return (
<Provider store={store}>
<MainContent />
</Provider>
);
}
Setting Up a Slice with Redux Toolkit
In Redux Toolkit, a slice manages the state
and reducers
. To create a slice, use the createSlice
function from @reduxjs/toolkit
.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
username: "",
email: "",
};
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {
usernameUpdated: (state, action) => {
state.username = action.payload;
},
emailUpdated: (state, action) => {
state.email = action.payload;
},
userDataCleared: (state) => {
state.username = "";
state.email = "";
},
},
});
export default userSlice.reducer;
The main arguments that createSlice
accepts are:
-
name
: A unique name for the slice. -
initialState
: The initial state of the slice. -
reducers
: thereducer
functions to handle updating the state.
Note: Have you noticed how we're directly modifying the state here? In createSlice
, we can use that syntax to simplifying the logic of updating the state - specially for the deeply-nested values. However, under the hood this is what it happens:
Redux Toolkit allows us to write "mutating" logic in reducers. It doesn't actually mutate the state because it uses the Immer library, which detects changes to a "draft state" and produces a brand new immutable state based off those changes.
But wait, how do we check for action
types and map them to the reducers
like we did in the previous code? Here’s the cool part: you don't need to! Once you define the reducers
, Redux Toolkit automatically generates the action creators for each one, and we can easily export them from the slice file like this:
// app/redux/userSlice.ts
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
username: "",
email: "",
};
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {
usernameUpdated: (state, action) => {
state.username = action.payload;
},
emailUpdated: (state, action) => {
state.email = action.payload;
},
userDataCleared: (state) => {
state = initialState;
},
},
});
// the actions are exported to be used by any component now
export const { usernameUpdated, emailUpdated, userDataCleared } =
userSlice.actions;
export default userSlice.reducer;
Then we can update the store
with the userSlice.reducer
:
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./userSlice";
export default configureStore({
reducer: {
user: userReducer,
// we can inroduce more reducers from different slices here
// posts: postsReducer
},
});
Using Selectors and Dispatching Actions
Now that the store and slice are set up, we can use selectors
to get the data and dispatch actions to update it.
The selectors
are to access the state data and make them available to the application's components. This is how to create and export them in the userSlice
file:
export const selectUsername = (state) => state.user.username;
export const selectEmail = (state) => state.user.email;
Now we can use the selector data and dispatch actions:
import { useSelector, useDispatch } from "react-redux";
import { selectUsername, selectEmail } from "@/redux/userSlice";
import { usernameUpdated, emailUpdated, userDataCleared } from "@/redux/userSlice";
const UserComponent = () => {
const username = useSelector(selectUsername);
const email = useSelector(selectEmail);
const dispatch = useDispatch();
const handleUpdateUsername = (name: string) => dispatch(usernameUpdated(name));
const handleUpdateEmail = (email: string) => dispatch(emailUpdated(email));
const handleClearUserData = () => dispatch(userDataCleared());
return (
<div>
<h2>Username: {username}</h2>
<p>Email: {email}</p>
<button onClick={() => handleUpdateUsername("new_username")}>Update Username</button>
<button onClick={() => handleUpdateEmail("new_email@example.com")}>Update Email</button>
<button onClick={handleClearUserData}>Clear User Data</button>
</div>
);
};
Top comments (0)