π Redux Toolkit Notes π
What is Redux?
Redux is a flexible state container for JS apps that manages our application state separately. It manages the application state in a single store, making it easier to handle complex state logic across the entire app.
Why Redux?
In normal flow, we need to do prop drilling to pass states in between components. Some levels donβt need the states here, which is a burden. Also uplifting a state for large medium apps isnβt a scalable solution as it requires structural changes. Thatβs why we need redux to manage states. All the states here are kept in store and whichever component needs that they can just subscribe to that store. Redux ensures predictable state management, easier debugging, and improved scalability by enforcing a unidirectional data flow.
Core Redux Components:
Action: An object that describes what happened. It typically contains a type and an optional payload. (A command)
Dispatch: A function used to send actions to the store to update the state. (A event occurring)
Reducer: A pure function that takes the current state and an action, then returns a new state. (Function that triggers when action dispatched)
Installing: npm i @reduxjs/toolkit react-redux
Redux Workflow:
Creating a Slice:
A slice is a collection of Redux reducer logic and actions for a single feature. The prepare callback allows US to customize the action payload before it reaches the reducer.
import { createSlice, nanoid } from "@reduxjs/toolkit";
const postSlice = createSlice({
name: "posts",
initialState: [],
reducers: {
addPost: {
reducer: (state, action) => {
state.push(action.payload);
},
prepare: (title, content) => ({
payload: { id: nanoid(), title, content },
}),
},
deletePost: (state, action) => {
return state.filter((post) => post.id != action.payload);
},
},
});
export const { addPost, deletePost } = postSlice.actions;
export default postSlice.reducer;
Creating store:
import { configureStore } from "@reduxjs/toolkit";
import postReducer from "../features/posts/postSlice";
export const store = configureStore({
reducer: {
posts: postReducer
},
});
Wrap with provider:
import { Provider } from "react-redux";
import { store } from "./app/store.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);
Use in Component:
const PostList = ({ onEdit }) => {
const posts = useSelector((state) => state.posts);
const dispatch = useDispatch();
return (
<div className="w-full grid grid-cols-1 gap-6 mt-12">
{posts.map((post) => (
<div key={post.id}></div>
))}
</div>
);
};
Redux Browser Extension: Redux DevTools
const store = configureStore({
reducer: rootReducer,
devTools: process.env.NODE_ENV !== 'production',
});
Async Operation in Redux (Redux Thunk):
In Redux, asynchronous operations (like API calls) are handled using middleware because Redux by default only supports synchronous state updates. The most common middlewares for handling async operations are Redux Thunk, Redux Toolkit (RTK) with createAsyncThunk, and Redux Saga.
Implementation:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Fetch all posts
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
return response.json();
});
// Initial State
const initialState = {
posts: [],
post: null,
loading: false,
error: null,
};
// Slice
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
// Fetch all posts
.addCase(fetchPosts.pending, (state) => {
state.loading = true;
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.loading = false;
state.posts = action.payload;
})
.addCase(fetchPosts.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
},
});
export default postsSlice.reducer;
Use Case:
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts, createPost, updatePost, deletePost } from './postsSlice';
const Posts = () => {
const dispatch = useDispatch();
const { posts, loading, error } = useSelector((state) =>state.posts);
useEffect(() => {
dispatch(fetchPosts());
}, [dispatch]);
const handleCreate = () => {
dispatch(createPost({ title: 'New Post', body: 'This is a new post' }));
};
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h1>Posts</h1>
<button onClick={handleCreate}>Create Post</button>
</div>
);
};
export default Posts;
Middleware
Middleware in Redux intercepts dispatched actions, allowing for logging, crash reporting, or handling async logic. Middleware lets us customize the dispatch process.
const blogPostMiddleware = (storeAPI) => (next) => (action) => {
if (action.type === 'posts/publishPost') {
const contentLength = action.payload.content.length;
if (contentLength < 50) {
console.warn('Post content is too short. Must be at least 50 characters.');
return;
}
console.log('Publishing post:', action.payload.title);
}
return next(action);
};
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(blogPostMiddleware),
});
Selectors
Selectors help access specific parts of the state.
export const selectCount = (state) => state.counter.value;
Error Handling
Handle errors effectively with proper state management.
initialState: {
items: [],
status: 'idle',
error: null,
},
.addCase(fetchData.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
RTK Query (Simplified Data Fetching)
RTK Query simplifies data fetching, caching, and synchronization. RTK Query automatically caches requests and avoids unnecessary refetching, improving performance.
Setting Up RTK Query
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
getPostById: builder.query({
query: (id) => `/posts/${id}`,
}),
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
}),
updatePost: builder.mutation({
query: ({ id, ...updatedPost }) => ({
url: `/posts/${id}`,
method: 'PUT',
body: updatedPost,
}),
}),
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE',
}),
}),
}),
});
export const {
useGetPostsQuery,
useGetPostByIdQuery,
useCreatePostMutation,
useUpdatePostMutation,
useDeletePostMutation,
} = api;
export default api;
Usage in Components
import React from 'react';
import {
useGetPostsQuery,
useCreatePostMutation,
useUpdatePostMutation,
useDeletePostMutation
} from './api';
const Blog = () => {
const { data: posts, error, isLoading } = useGetPostsQuery();
const [createPost] = useCreatePostMutation();
const [updatePost] = useUpdatePostMutation();
const [deletePost] = useDeletePostMutation();
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
const handleCreate = async () => {
await createPost({ title: 'New Post', body: 'Content of the post', userId: 1 });
};
const handleUpdate = async (id) => {
await updatePost({ id, title: 'Updated Title', body: 'Updated content' });
};
const handleDelete = async (id) => {
await deletePost(id);
};
return (
<div>
<button onClick={handleCreate}>Create Post</button>
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.title}
<button onClick={() => handleUpdate(post.id)}>Edit</button>
<button onClick={() => handleDelete(post.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default Blog;
Immutable Updates with Immer
Immer allows us to write logic that "mutates" state directly while keeping the updates immutable under the hood.
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => {
state.value += 1; // Mutates state directly (handled by Immer)
},
Mutate vs. Immutable
Mutate: Changing the data directly. For example, modifying an object or array.
Immutable: Instead of modifying data directly, we create a new copy with the changes applied, leaving the original data untouched.
How Immer Works
Immer helps us write code that looks like we're mutating data (i.e., changing it directly), but it automatically keeps the changes immutable under the hood. This is useful for avoiding common bugs when dealing with immutable data structures in JavaScript.
Example: Without Immer (mutation):
let state = { value: 0 };
// Mutating the state directly
state.value += 1; // This is mutation
console.log(state.value); // 1
With Immer (immutability):
import produce from 'immer';
let state = { value: 0 };
// Using Immer's `produce` to "mutate" the state
const nextState = produce(state, draft => {
draft.value += 1; // Looks like mutation, but Immer keeps it immutable
});
console.log(state.value); // 0 (original state is unchanged)
console.log(nextState.value); // 1 (new state with the update)
This makes working with Redux (or any state management) easier because we donβt have to clone and update the state manually; Immer does it for us automatically.
Redux Persist:
To persist Redux state across page refreshes, we can integrate Redux Persist. This will store your Redux state in local storage or session storage and reload it when the app is refreshed.
Install:
npm install redux-persist
Implement:
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
// Defaults to localStorage
import { combineReducers } from 'redux';
import postReducer from './postSlice';
// Persist configuration
const persistConfig = {
key: 'root',
storage,
};
// Combine reducers (in case of multiple reducers)
const rootReducer = combineReducers({
posts: postReducer,
});
// Persisted reducer
const persistedReducer = persistReducer(persistConfig, rootReducer);
// Configure store
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false, // Required for redux-persist
}),
});
// Persistor for persisting the store
export const persistor = persistStore(store);
export default store;
Wrap with Persisit Gate:
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>
Optional Enhancements
Use sessionStorage Instead of localStorage:
Change the storage to session-based (clears when the browser closes):
import storageSession from 'redux-persist/lib/storage/session';
const persistConfig = { key: 'root', storage: storageSession };
Selective Persistence:
Only persist specific slices of the state:
const persistConfig = {
key: 'root',
storage,
whitelist: ['posts'], // Only persist posts
};
I have created a simple blog project with react, redux and ant design having CRUD functionality. You can check it out.
Project Link - Redux Blog App
π― Master Redux Toolkit and elevate your React apps!
Top comments (0)