I've been using Redux for managing and updating the global state in my React apps for a while. Redux makes it way easier to manage the gobal state and automatically implements complex performance optimizations, so that your own component only re-renders when the data it needs has actually changed ; the major complaint about the React-Redux is ** boilerplate**. 🍽️
From configuring store to configuring with redux dev tools and maintaining actions, dispatch and reducers is quite hectic.
To overcome these problems we might choose two solutions :
1. Stop using REDUX !! ⚠️
Yes ! you might not even need Redux . There is inbuild Context API with React where we can basically inject the state data into the different components. Honestly, most of the simpler apps doesn't even need redux. You can use libraries like react-query for caching data with Context for passing data among components.
2. Use Redux Toolkit(RTK):
Redux toolkit is the official, opinionated, batteries-included toolset for efficient Redux development.
Redux Toolkit was originally created to help address three common concerns about Redux:
- "Configuring a Redux store is too complicated"
- "I have to add a lot of packages to get Redux to do anything useful"
- "Redux requires too much boilerplate code"
It includes utilities to simplify common use cases like store setup, creating reducers, immutable update logic, and more. Also, it follows the DUCK 🦆 pattern which has way less of the boiler-plate code.
Redux addons are builtin like Redux DevTools Extension & Redux Thunk in RTK. 🤯🤯
Before heading towards the code part you must have the basic knowledge of Redux terminologies like :
- store
- actions
- reducer
- Provider component
If you haven't already checkout the latest basic tutorial section in React-Redux docs. They have recently updated it and make fantastic tutorials for getting started with redux.
Installation
Start with the official Redux+JS template for Create React App, which takes advantage of React Redux's integration with React components.
npx create-react-app my-app --template redux
or,
You can add the following packages in existing React application.
yarn add redux react-redux @reduxjs/toolkit
Here we are going the visualize the basic todo app and demonostrate the redux part of it.
configureStore()
This API wraps createStore
to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includes redux-thunk by default, and enables use of the Redux DevTools Extension
. Yep ! you heard it right, it configures Redux dev tools out of box.
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "../slices/todoSlice";
export default configureStore({
reducer: {
todos: todoReducer,
},
});
Here we're importing configureStore()
then
pass the todoReducer
into the root reducer and configure root reducer to store. We need to call state.todos
to access all the values in todos.
createSlice()
creatSlice()
is basically collection of Redux reducer logic and actions for single feature in your app. It automatically creates the action types strings, so no action types need to be created for reducers.
// todoSlice.js
import { createSlice } from "@reduxjs/toolkit";
export const todoSlice = createSlice({
name: "todos",
initialState: {
todoList: [],
},
reducers: {
addTodo: {},
},
});
export const { addTodo } = todoSlice.actions;
export default todoSlice.reducer;
It basically takes three things:
- name : A string name for this slice of state. Generated action type constants will use this as a prefix.
- initial State : set the todolist to an empty array
- reducers object
Finally we export the actions and reducer.
AddTodo Reducer
Basically, it takes the state, action and push the payload into the state.
addTodo: {
reducer: (state, action) => {
state.todoList.push(action.payload);
},
},
Previously, what we used to do is not mutating the state cuz' state in redux is immutable.
But Redux Toolkit
uses Immer
under the hood so we can directly mutate the data. So, here we are directly pushing the data from payload in the todolist.
useDispatch()
When we call the data in out component we use the hook useDispatch()
.
I'm just showing the sample code here how to use it.
// AddTodo Component
const dispatch = useDispatch();
import { addTodo } from "../../redux/slices/todoSlice";
const handleSubmit = () => {
dispatch(addTodo(value));
};
Here we are using useDispatch()
hook to dispatch the addTodo action.
Now, we have added the Todo in the state successfully. 🕺
Add other fields to payload
In react if we want to keep the list sync with the state, then we have to add the key to each list. For that we have to add the prepare callback
.
// todoSlice.js
import { createSlice, nanoid } from "@reduxjs/toolkit";
export const todoSlice = createSlice({
name: "todos",
initialState: {
todoList: [],
},
reducers: {
addTodo: {
reducer: (state, action) => {
state.todoList.push(action.payload);
},
prepare(value) {
return {
payload: {
key: nanoid(),
value: value,
},
};
},
},
},
});
export const { addTodo } = todoSlice.actions;
export default todoSlice.reducer;
Import nanoid()
from redux toolkit and add the prepare callback where we pass the key as nanoid()
and value
as todo-value.
useSelector()
If you are familiar with redux hooks then useSelector()
hooks basically selects the state from store and load it into the component.
// TodoList component
import { useSelector } from "react-redux";
const todoListdata = useSelector((state) => state.todos.todoList);
//... map the todoListdata
Fetch Data from an Api
Previously, we have to use the middlewares like redux-thunk
or redux-saga
for fetching the data from external api. Redux toolkit simplifies it by using the built in redux-thunk
unde the hood and using createAsyncThunk
function.
createAsyncThunk
is a function that accepts a Redux action type string and a callback function that should return a promise. It generates promise lifecycle action types based on the action type prefix that you pass in, and returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise.
createAsyncThunk
creates three types of actions automatically:
- pending ⏳
- fulfilled ✅
- rejected ❌
// todoSlice.js
import { createSlice, nanoid, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchTodos = createAsyncThunk("todos/fetchTodos", async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/todos/",
);
return data;
});
Here we use Axios
to fetch the data and createAsyncThunk
. createAsyncThunk
is taking two params :
- name of action type :
'todos/fetchTodos'
which appends the three action types (pending, fullfilled & rejected). - callback function which does the async actions and returns the todos data.
Adding Status and Error to Initial State
We add two status
state and error
to the initial state to check if the action is idle and if there is any error in result.
// todoSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const fetchTodos = createAsyncThunk("todos/fetchTodos", async () => {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos/",
);
return response.data.todoList;
});
export const todoSlice = createSlice({
name: "todos",
initialState: {
todoList: [],
status: "idle",
error: null,
},
reducers: {},
extraReducers: {
[fetchTodos.pending]: (state, action) => {
state.status = "loading";
},
[fetchTodos.fulfilled]: (state, action) => {
state.status = "succeeded";
state.todoList.push(...action.payload);
},
[fetchTodos.rejected]: (state, action) => {
state.status = "failed";
state.error = action.error.message;
},
},
});
export const { addTodo } = todoSlice.actions;
export default todoSlice.reducer;
Create async thunk is creating its own actions we need extraReducers as fourth parameter in createSlice()
which can take those actions and update the state correspondigly. It basically check the status of the state and map the status and error in the state accordingly. If the data is fetched successfully, then push it into todolist.
Now we can check the status of data and map the data in the corresponding component.
That's it ! 👋
Tbh, Redux toolkit seems a little bit confusing at first but once you get hands on it it will reduce a lot of boiler plate in your application.