Redux Demystified
Vedant Lahane
Posted on July 8, 2022
What is Redux?
Redux is a predictable container for JavaScript apps.
Redux is for JavaScript applications
Redux is not tied to React. Can be used with Angular, Vue, or even vanilla JS.
Redux is a state container
Redux stores the state of your application.
The state of an application is the state shared by all the individual components of that application.
Redux will store and manage the application state.
Redux is predictable
Redux is a state container and in any JavaScript application, the state of the application can change.
In Redux, a pattern is enforced to ensure that all the state transitions are explicit and can be tracked.
Why Redux?
Redux will help you manage the global state of your application in a predictable way.
The patterns and tools provided by Redux make it easier to understand when, where, why & how the state in your application is being updated.
Redux guides you towards writing code that is predictable and testable.
What is Redux Toolkit?
Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.
It is also intended to be the standard way to write Redux logic in your application.
Why Redux Toolkit?
Redux is great, but it has a few shortcomings:
- Configuring Redux in an app seems complicated.
- In addition to Redux, a lot of other packages have to be installed to get Redux to do something useful.
- Redux requires too much boilerplate code.
Redux Toolkit serves as an abstraction over Redux. It hides the difficult parts ensuring you have a good developer experience.
React-Redux
Summary of what we’ve learned so far
- React is a library to build user interfaces.
- Redux is a library for managing the state in a predictable way in JS apps.
- Redux Toolkit is a library for efficient redux development.
- React-Redux is a library that provides bindings to use React and Redux Toolkit together in an application.
Caveats
- Never learn React and Redux in parallel.
- “When to use Redux in your application?” Redux helps you deal with shared state management but like any tool, it has certain trade-offs. Pros
- You have large amounts of application state that are needed in many places in the app.
- The app state is updated frequently over time.
- The logic to updating that state may be complex
- The app has a medium or large-sized codebase and might be worked on by many people. Cons
- There are more concepts to learn and more code to write.
- It also adds some indirections to your code and asks you to follow certain restrictions.
- It’s a trade-off between long-term and short-term productivity.
Prerequisites
React Fundamentals
React Hooks
Getting Started with Redux
- Install node.js if you haven’t already. Here’s the link https://nodejs.org/en/
- Create a folder
learn-redux
or any other name on your desktop. - Open the folder in your code editor, preferably Visual Studio Code.
- Inside the folder, in your terminal, enter the command
npm init --yes
This will initialize apackage.json
file with the default settings. For reference,PS E:\GitHub\learn-redux> npm init --yes
- Add redux as a dependency for your project. Enter the command
npm-install-redux
in your terminal. For reference,PS E:\GitHub\learn-redux> npm install redux
- Create an
index.js
inside your folder.
That’s it! We are all set to get our hands dirty in Redux Toolkit 🚀
Three Core Concepts
- A store that holds the state of your application.
- An action that describes what happened in the application.
- A reducer is what ties the store and actions together. It handles the action and decides how to update the state.
Let’s consider an example of a Cake Store.
- A store is similar to a cake store in the sense that the cake store has a number of cakes in its store inventory. On the other hand, a redux store has its states in its store.
- An action is when a customer places an order for a cake. In that case, an order has been placed and the count of the cakes has to be reduced by one.
- A reducer in our case is a shopkeeper. He receives the order from the customer, which is an action and removes the cake from the shelf which is a store.
Three Principles
- First Principle - The global state of your application is stored as an object inside a single store. In simpler terms, maintain our application state in a single object which would be managed by the Redux store.
- Second Principle - The only way to change the state is to dispatch an action, an object that describes what happened. Thus, to update the state of your app, you need to let Redux know about that with an action. One should not directly update the state object.
- Third Principle - To specify how the state tree is updated based on actions, you write pure reducers. The reducer takes the previous state and an action and returns a new state.
Reducer - (previousState, action) ⇒ newState
Let’s get back to our Cake Shop.
- Let’s assume we are tracking the number of cakes on the shelf. So our object would look something like this.
// A redux store as per the First Principle
{
numberOfCakes: 10
}
- A common action would be scanning the QR code to place an order for a cake. This action would look like the one below.
// A redux action as per the Second Principle
{
type: 'CAKE_ORDERED'
}
- A reducer could be a shopkeeper in our case. The shopkeeper performs the action of placing an order and then reduces the cake count. Just like this reducer below.
const reducer = (state = inititalState, action) => {
switch (action.type) {
case CAKE_ORDERED:
return {
numberOfCakes: state.numberOfCakes - 1
}
}
}
Three Principles Overview
Diving deeper into the Three’s
Actions
- The only way your application can interact with the store.
- Carry some information from your app to the redux store.
- Plain Javascript objects.
- Have a
type
property that describes something that happened in the application. - The
type
property is typically defined as string constants. - An action creator is a function that returns an object.
Reducers
- Reducers specify how the app’s state changes in response to the actions sent to the store.
- Reducer is a function that accepts state and action as arguments and returns the next state of the application.
(previousState, action) ⇒ newState
Store
- One store for the entire application.
- Responsibilities of a Redux store:
- holds the application state
- allows access to the state via
getState()
- allows the state to be updated via
dispatch(action)
- registers listeners via
subscribe(listener)
- handles unregistering of the listeners via the function returned by
subscribe(listener)
Bind Action Creators
The first argument is an object where we define different action creators.
The second argument is what we want to bind those actions to.
const bindActionCreators = redux.bindActionCreators()
const actionCreatorOne = (paramOne = 1) => {
return {
type: "ACTION_ONE",
payload: paramOne
}
}
const actions = bindActionCreators({ actionCreatorOne(), actionCreatorTwo() }, store.dispatch)
actions.actionCreatorOne()
actions.actionCreatorTwo()
Although bind action creators are not necessary, redux does bring it along with all of its other packages.
Combine Reducers
const combineReducers = redux.combineReducers
const rootReducer = combineReducers({
keyOne: // reducerOne,
keyTwo: // reducerTwo
})
const store = createStore(rootReducer)
combineReducers
take an object as an argument. The object has keys as any name and the values as a reducer function.
When we dispatch an action, both the reducers receive that action. The difference is that one of them acts on the action whereas the other just ignore it.
Now by doing what we have just done, each of the reducers is managing its own part of the application global state.
The state parameter is different for every reducer and corresponds to the part of the state it manages.
When your app grows, you can split the reducers into different files and keep them completely independent, managing different features. For example, authReducer, a userReducer, profileReducer, etc.
Immer
In a Redux environment, we learned to never mutate the object state.
Here’s how we achieved the same.
const cakeReducer = (state = initialCakeState, action) => {
switch (action.type) {
case CAKE_ORDERED:
return {
...state, // spread operator to make a copy of all the properties
numberOfCakes: state.numberOfCakes - 1, // only update the desired property
};
case CAKE_RESTOCKED:
return {
...state,
numberOfCakes: state.numberOfCakes + action.payload,
};
default:
return state;
}
};
In practical applications, the state is more complex with nested levels, and in such situations, updating the state could be troublesome.
Immer simplifies handling immutable data structures.
To install immer
enter the npm install immer
command in your terminal.
const personalData = {
name: "Vedant",
address: {
street: "123 Main St",
city: 'Boston',
state: 'MA',
}
}
{
...personalData,
address: {
...personalData.address,
street: "789 Main St"
}
}
produce(personalData, (draft) => {
draft.address.street = "789 Main St"
})
Middleware
It is the suggested way to extend Redux with custom functionality.
Provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
Middleware is usually used for logging, crashing, reporting, performing asynchronous tasks, etc.
Let’s check out the logger
middleware. To use logger
, enter the command npm i redux-logger
in the terminal.
redux-logger
Log all information related to redux in your application.
const applyMiddleware = redux.applyMiddleware
const reduxLogger = require("redux-logger")
const logger = reduxLogger.createLogger()
const store = createStore(rootReducer, applyMiddleware(logger))
Async Actions
Try recollecting the cake shop scenario again. So, the following were the events occurring in a cake shop.
As soon as an action was dispatched, the state was immediately updated.
Thus, if you dispatch the CAKE_ORDERED
action, the numberOfCakes
was right away decremented by 1.
Same with the ICECREAM_ORDRERED
action as well.
All the above actions were synchronous actions.
Asynchronous actions comprise asynchronous API calls to fetch data from an endpoint and use that data in your application.
What next?
Let’s have our application fetch a list of users from an API endpoint and store the list in a redux store. We already know that there exists the state, the actions, and the reducers as the three main concepts in any redux app.
A typical state in our app would look like,
// State
state = {
loading: true,
data: [],
error: '',
}
// loading - Display a loading spinner in your component
// data - List of users
// error - Display error to the user
Here are some common actions,
// Actions
FETCH_USERS_REQUESTED - // Fetch the list of users
FETCH_USERS_SUCCEEDED - // Fetched successfully
FETCH_USERS_FAILED - // Error when fetching the data
These are the reducers,
// Reducers
case: FETCH_USERS_REQUESTED
loading: true
case: FETCH_USERS_SUCCEEDED
loading: false
users: data // (from API)
case: FETCH_USERS_FAILED
loading: false
error: error // (from API)
Redux Thunk Middleware
Let’s learn how to define an asynchronous action creator using axios
& redux-thunk
.
axios
- requests to an API endpoint
redux-thunk
- a middleware to define async action creators
Thunk middleware brings to the table the ability for an action creator to return a function instead of an action object.
Also, the function need not be pure. It means that the function can consist of API calls.
It has dispatch method as its arguments and thus can dispatch actions as well.
const redux = require("redux")
const thunkMiddleware = require("redux-thunk").default
const axios = require("axios")
const createStore = redux.createStore
const applyMiddleware = redux.applyMiddleware
const initialState = {
loading: false,
users: [],
error: "",
}
const FETCH_USERS_REQUESTED = "FETCH_USERS_REQUESTED"
const FETCH_USERS_SUCCEEDED = "FETCH_USERS_SUCCEEDED"
const FETCH_USERS_FAILED = "FETCH_USERS_FAILED"
const fetchUsersRequest = () => {
return {
type: FETCH_USERS_REQUESTED,
}
}
const fetchUsersSuccess = users => {
return {
type: FETCH_USERS_SUCCEEDED,
payload: users,
}
}
const fetchUsersFailure = error => {
return {
type: FETCH_USERS_FAILED,
payload: error,
}
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case FETCH_USERS_REQUESTED:
return {
...state,
loading: true,
}
case FETCH_USERS_SUCCEEDED
return {
...state,
loading: false,
users: action.payload,
error: "",
}
case FETCH_USERS_FAILED
return {
...state,
loading: false,
users: [],
error: action.payload,
}
default:
return state
}
}
const fetchUsers = () => {
return async function(dispatch) {
dispatch(fetchUsersRequest())
try {
const { data: users } = await axios.get("https://jsonplaceholder.typicode.com/users")
dispatch(fetchUsersSuccess(users))
} catch (error) {
dispatch(fetchUsersFailure(error.message))
}
}
}
const store = createStore(reducer, applyMiddleware(thunkMiddleware))
store.subscribe(() => console.log(store.getState()))
store.dispatch(fetchUsers())
Now you might ask, “All this is good. So why Redux Toolkit?”
Below is the answer to your question.
Redux concerns
Redux requires too much boilerplate code.
- action
- action object
- action Creator
- switch statement in a reducer
A lot of other packages have to be installed to work with Redux.
- redux-thunk
- immer
- redux devtools
Therefore, Redux Toolkit!
Redux Toolkit
Redux toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.
- abstract over the setup process
- handle the most common use cases
- include some useful utilities
Getting started with Redux Toolkit
- Create a folder
redux-toolkit-demo
or any other name on your desktop. - Open the folder in your code editor, preferably Visual Studio Code.
- Inside the folder, in your terminal, enter the command
npm init --yes
This will initialize apackage.json
file with the default settings. For reference,PS E:\GitHub\learn-redux> npm init --yes
- Add redux as a dependency for your project. Enter the command
npm i @reduxjs/toolkit
in your terminal. For reference,PS E:\GitHub\learn-redux> npm i @reduxjs/toolkit
- Create an
index.js
inside your folder.
Opinionated folder structure for Redux Toolkit
- Create an
index.js
inside yourredux-toolkit-demo
folder. - Create a folder
app
insideredux-toolkit-demo
. - Create a file
store.js
inside theapp
folder. This file will contain code related to our redux store. - Create another folder named
features
on the same level as theapp
folder. This folder will contain all features of our application.
And you’re done!
Slice
Group together the reducer logic and actions for a single feature in a single file. And, that file name must contain Slice
in its suffix.
The entire application state is split into slices and managed individually.
const createSlice = require("@reduxjs/toolkit").createSlice // ES Module import
const initialState = {
// initial state object
}
const someSliceName = createSlice({
name: // any name,
initialState: // the initial state,
reducers: {
// reducer actions
actionName: (state, action) => {
state.propertyName = // any value // Direct state mutation possible
}
}
})
module.exports = someSliceName.reducer // default export
module.exports.someActionName = someSliceName.actions // named export
-
createSlice
under the hood uses the immer library. Thus, Redux Toolkit handles the state update on our behalf. -
createSlice
will automatically generate action creators with the same name as the reducer function (here,actionName
) we have written. -
createSlice
also returns the main reducer function which we can provide to our redux store. -
createSlice
abstracts all of the boilerplate code of writing the action type constants, action object, action creators, and switch cases and also handles immutable updates.
Configuring Store
-
configureStore
takes an object as an argument. - The object has a key
reducer
and this reducer is where we specify all the reducers.
const configureStore = require("@reduxjs/toolkit").configureStore; // similar to createStore in redux
const store = configureStore({
reducer: {
reducerOneName: // reducerOne,
},
});
Middleware
const { getDefaultMiddleware } = require("@reduxjs/toolkit");
const reduxLogger = require("redux-logger");
const store = configureStore({
reducer: {
reducerOneName: // reducerOne,
reducerTwoName: // reducerTwo,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
});
Example of the logger middleware terminal
Initial State { cake: { numberOfCakes: 10 }, icecream: { numberOfIcecreams: 20 } }
action cake/ordered @ 23:31:25.354
prev state { cake: { numberOfCakes: 10 }, icecream: { numberOfIcecreams: 20 } }
action { type: 'cake/ordered', payload: undefined }
next state { cake: { numberOfCakes: 9 }, icecream: { numberOfIcecreams: 20 } }
action cake/ordered @ 23:31:25.357
prev state { cake: { numberOfCakes: 9 }, icecream: { numberOfIcecreams: 20 } }
action { type: 'cake/ordered', payload: undefined }
next state { cake: { numberOfCakes: 8 }, icecream: { numberOfIcecreams: 20 } }
action cake/ordered @ 23:31:25.359
prev state { cake: { numberOfCakes: 8 }, icecream: { numberOfIcecreams: 20 } }
action { type: 'cake/restocked', payload: 2 }
next state { cake: { numberOfCakes: 10 }, icecream: { numberOfIcecreams: 20 } }
The type
property has a slice name as the first part and the key of each reducer function as the second part, separated by a “/”.
Thus, cake
is a slice name and there are reducer functions ordered
& restocked
.
Async Actions
- Asynchronous actions in RTK are performed using
createAsyncThunk
method. -
createAsyncThunk
method has two arguments. - The first argument is the action name.
- The second argument is a callback function that creates the payload.
-
createAsyncThunk
automatically dispatches lifecycle actions based on the returned promise. A promise has pending, fulfilled or rejected. Thus,createAsyncThunk
returns a pending, fulfilled or rejected action types. - We can listen to these actions types by a reducer function and perform the necessary state transitions.
- The reducers though are not generated by the slice and have to be added as extra reducers.
const createSlice = require("@reduxjs/toolkit").createSlice;
const createAsyncThunk = require("@reduxjs/toolkit").createAsyncThunk;
const axios = require("axios");
const initialState = {
loading: false,
users: [],
error: "",
};
//Generates pending, fulfilled and rejected action types.
const fetchUsers = createAsyncThunk("user/fetchUsers", () => {
return axios
.get("https://jsonplaceholder.typicode.com/users")
.then((response) => response.data.map((user) => user.id));
});
// example - a simple user slice
const userSlice = createSlice({
name: "user",
initialState,
extraReducers: (builder) => {
builder.addCase(fetchUsers.pending, (state) => {
state.loading = true;
});
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
state.error = "";
});
builder.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.users = [];
state.error = action.error.message;
});
},
});
module.exports = userSlice.reducer;
module.exports.fetchUsers = fetchUsers;
React Redux setup
- Create a react project
Now, we could also use
create-react-app
but let’s try this new frontend tooling library vite. - Inside the root folder, in your terminal, enter the command
npm create vite@latest project-name
This will initialize a react app namedproject-name
. - Make the terminal point to the react project directory by entering the command
cd project-name
in the terminal. - Inside the folder, in your terminal, enter the command
npm install
This will install all the required packages inpackage.json
file in your app. - Copy & Paste the
app
andfeatures
folders from yourredux-toolkit-demo
folder into thesrc
sub-folder of the newly created react app. - Install the required dependencies -
axios
,createSlice
,createAsyncThunk
- Start the server by entering the command
npm run dev
Provider
- Install the react-redux package in your folder. Enter the following command
npm i react-redux
- Restart the server by entering the command
npm run dev
. - We need to make available the store to the react app component tree. This is where the
react-redux
library comes into the picture. -
react-redux
library exports a component calledprovider
. - Firstly import the provider component from
react-redux
library Like this,
// main.jsx
import { Provider } from "react-redux
import store from "./app/store"
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider>
<App />
</Provider>
</React.StrictMode>
)
- It is very important to note that the
Provider
component should be present at the top of all the components. Thus the propsstore
is provided to every component in the app. - This is because the Provider component uses
React Context
under the hood.
useSelector
- The
useSelector
hook is used to get hold of any state that is maintained in the redux store. - It is sort of a wrapper around
store.getState()
// CakeView.jsx
import React from "react"
import { useSelector } from "react-redux"
export const CakeView = () => {
const numberOfCakes = useSelector((state) => state.cake.numberOfCakes)
return (
<div>
<h2>Number of Cakes - {numberOfCakes}</h2>
<button>Order cake</button>
<button>Restock cakes</button>
</div>
)
}
useDispatch
- The
useDispatch
hook is used to dispatch an action in React-Redux. - The hook returns a reference to the dispatch function from the redux store.
// IcecreamView.jsx
import React from "react"
import { useState } from "react"
import { useSelector, useDispatch } from "react-redux"
import { ordered, restocked } from "./icecreamSlice"
export const IcecreamView = () => {
const [value, setValue] = useState(1)
const numberOfIcecreams = useSelector((state) => state.icecream.numberOfIcecreams)
const dispatch = useDispatch()
return (
<div>
<h2>Number of icecream - {numberOfIcecreams} </h2>
<button onClick={() => dispatch(ordered())}>Order cake</button>
<input type="number" value={value} onChange={(e) => setValue(parseInt(e.target.value))}/>
<button onClick={() => dispatch(restocked(value))}>Restock icecream</button>
</div>
)
}
// UserView.jsx
import React, {useEffect} from "react"
import { useSelector, useDispatch } from "react-redux"
import { fetchUsers } from "./userSlice"
export const UserView = () => {
const user = useSelector((state) => state.user)
const dispatch = useDispatch()
useEffect(() => {
dispatch(fetchUsers())
}, [])
return (
<div>
<h2>List of Users</h2>
{user.loading && <div>Loading...</div>}
{!user.loading && user.error ? <div>Error: {user.error}</div> : null}
{!user.loading && user.users.length ? (
<ul>
{user.users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
) : null}
</div>
)
}
That's all!
There's a simple analogy I believe in & I'd like to share it with y'all.
- The curiosity that lead you to search for something is the first but I'd say the most important part. Remember, you are that time old when you started learning something say XYZ.
- Consuming the learning material (a blog, a video, or some documentation, etc.) effectively is the next important step.
- The application part of learning something is the one that the majority fails at.
I can't emphasize more on how important it is to apply the learnings. So, after learning Redux, I made a social-media app wherein I used Redux Toolkit.
Live: https://jurassic-world.netlify.app
GitHub Repo: https://github.com/MarkVed17/jurassic-world
Dropping my repository link whilst I started with Redux.
https://github.com/MarkVed17/learn-redux
Now, if you are someone who has already stepped in the React ecosystem for a while now, you might've come across the React Context API versus Redux for state management. There's a lot of ground to cover this one. So, let's keep that topic of debate for some other day.
Until then, Keep learning! Keep growing! 😎
Let's connect on LinkedIn & Twitter.
Resources
Assets Credits
Posted on July 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.