Learning Redux for React: A Comprehensive Guide (2025 Edition)
Overall Guidelines:
- Target Audience: Absolute beginners with no prior knowledge of Redux or advanced React state management.
- Clarity and Simplicity: Explanations will prioritize clear, straightforward language, with jargon explained thoroughly when introduced.
- Logical Progression: Topics are arranged to build understanding step-by-step.
- Markdown Format: Proper Markdown formatting is used throughout.
Document Structure and Content:
1. Introduction to Redux for React
What is Redux for React?
Redux is an open-source JavaScript library for managing and centralizing application state. In the context of React, Redux acts as a predictable state container, providing a single source of truth for your application’s global state. This means all your application’s data that needs to be shared across multiple components resides in one place.
While React components have their own local state (managed by useState or useReducer), Redux steps in when that local state needs to be shared or accessed by many components, especially those that are not directly related (e.g., deeply nested components or components in different parts of your application).
Redux is often used in conjunction with Redux Toolkit (RTK), which is the official, opinionated, and recommended way to write Redux logic. RTK simplifies common Redux tasks, reduces boilerplate code, and incorporates best practices, making Redux significantly easier to learn and use, especially for beginners.
Why Learn Redux for React? (Benefits, Use Cases, Industry Relevance)
As of 2025, Redux remains highly relevant for large and complex React applications. Here’s why:
- Centralized State Management: All your application’s state lives in one “store,” making it easy to understand and debug. No more “prop drilling” (passing data through many layers of components).
- Predictable State Updates: Redux enforces a strict, unidirectional data flow. Every state change is triggered by an “action” and processed by a “reducer,” ensuring that state updates are predictable and traceable. This helps prevent bugs and makes your application’s behavior consistent.
- Scalability: As your application grows in complexity and size, Redux provides a structured and organized way to manage state, allowing multiple teams to work on different parts of the application without stepping on each other’s toes.
- Powerful Debugging Tools: The Redux DevTools Extension is a game-changer. It allows you to inspect every state change, “time-travel” through previous states, replay actions, and debug your application’s state mutations effectively.
- Industry Relevance: Redux is a widely adopted library in the industry, particularly for large-scale enterprise applications. Learning Redux (especially with Redux Toolkit) makes you a more versatile and employable React developer.
- Handling Complex Asynchronous Logic: While React Query (TanStack Query) is excellent for server-side data fetching and caching, Redux Toolkit, particularly with
createAsyncThunkandcreateListenerMiddleware, provides robust solutions for managing complex client-side asynchronous workflows, UI states, and business logic that needs to persist across the application. Many enterprise applications use both Redux Toolkit and React Query in tandem: Redux Toolkit for local UI state and complex business logic, and React Query for managing server data.
A Brief History (Concise)
Redux was created by Dan Abramov and Andrew Clark in 2015, inspired by Facebook’s Flux architecture and Elm. It aimed to provide a more predictable state management pattern. Initially, Redux involved a fair amount of boilerplate code. To address this, Redux Toolkit was introduced, which became the official and recommended way to write Redux logic, significantly simplifying its usage and reducing common pitfalls.
Setting Up Your Development Environment
To get started with Redux for React, you’ll need Node.js and npm (Node Package Manager) or Yarn installed on your machine. We’ll use Vite for a quick and modern React setup.
Prerequisites:
- Node.js (LTS version recommended): Download from nodejs.org. This includes npm.
- A code editor: Visual Studio Code is highly recommended.
Step-by-step instructions:
Create a new React project with Vite: Open your terminal or command prompt and run the following command:
npm create vite@latest my-redux-app -- --template reactmy-redux-appis the name of your project. You can choose any name.--template reactspecifies that we want a React project.
Navigate into your project directory:
cd my-redux-appInstall project dependencies:
npm installInstall Redux Toolkit and React-Redux: These are the core libraries you’ll need for Redux in a React application.
npm install @reduxjs/toolkit react-redux@reduxjs/toolkit: The official, recommended way to write Redux logic, providing utilities that simplify development.react-redux: The official React bindings for Redux, allowing your React components to interact with the Redux store.
Start your development server (optional, for testing setup):
npm run devThis will start a local development server, usually at
http://localhost:5173. You should see a basic React app running.
Your development environment is now set up and ready for Redux!
2. Core Concepts and Fundamentals
Redux, especially with Redux Toolkit, revolves around a few core concepts: Store, Actions, and Reducers (Slices). Understanding these building blocks is crucial for effective state management.
The Redux Store: The Single Source of Truth
The Redux Store is the heart of your Redux application. It’s a single JavaScript object that holds your entire application’s state tree. All data that needs to be shared or accessed globally throughout your application will reside here.
Key Characteristics of the Store:
- Single: There should only be one Redux store in your application.
- Centralized: It acts as a central hub for all application state, making it predictable and easier to manage.
- Immutable (conceptually): While Redux Toolkit uses Immer to allow “mutating” state directly, internally, Redux always works with new state objects, never directly modifying the existing state. This immutability is key for predictable updates and powerful debugging.
How to Create the Store with Redux Toolkit:
Redux Toolkit provides the configureStore function to set up your Redux store with good defaults, including automatic integration with Redux DevTools and common middleware like redux-thunk (for handling asynchronous logic).
Let’s create our store:
Create a
store.jsfile: Inside yoursrcfolder, create a new folder namedappand then a filestore.jsinside it (src/app/store.js).// src/app/store.js import { configureStore } from '@reduxjs/toolkit'; // We'll add our reducers here later export const store = configureStore({ reducer: { // This is where we'll register our "slices" }, }); // For TypeScript users, you'd also export types here: // export type RootState = ReturnType<typeof store.getState>; // export type AppDispatch = typeof store.dispatch;Explanation:
configureStore: This function is the primary way to create a Redux store with Redux Toolkit. It handles a lot of the boilerplate setup for you.reducer: This property takes an object where you will define the different “slices” of your application’s state. Each key in this object will correspond to a part of your state tree.
Provide the Store to Your React Application: For your React components to access the Redux store, you need to wrap your root React component (usually
App.jsxormain.jsxfor Vite) with theProvidercomponent fromreact-redux.Update your
src/main.jsx(orsrc/index.jsfor Create React App):// src/main.jsx import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './index.css'; import { Provider } from 'react-redux'; // Import Provider import { store } from './app/store.js'; // Import the store ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> {/* Provider makes the Redux store available to any nested components */} <Provider store={store}> <App /> </Provider> </React.StrictMode> );Explanation:
Provider: This component fromreact-reduxtakes your Reduxstoreas a prop and makes it available to all descendant React components. This eliminates the need for prop drilling.
Actions: Describing What Happened
In Redux, Actions are plain JavaScript objects that describe what happened in your application. They are the only way to send data to the Redux store. Think of them as messages or events.
Key Characteristics of Actions:
- Plain Objects: Actions are simple JavaScript objects.
typeProperty: Every action must have atypeproperty, which is a string constant. This string describes the type of action being performed (e.g.,'counter/increment','todo/added'). Thistypeis how reducers determine how to update the state.payloadProperty (Optional): Actions often contain additional data needed to update the state. This data is typically passed in apayloadproperty.
Example of an Action:
// A simple action to increment a counter
{
type: 'counter/increment',
// No payload needed for a simple increment
}
// An action to add a todo item
{
type: 'todo/added',
payload: {
id: 'unique-id-123',
text: 'Learn Redux',
completed: false
}
}
Action Creators:
While you could manually create action objects, it’s a common practice to use Action Creators. These are functions that simply return an action object. Redux Toolkit’s createSlice automatically generates action creators for you, making this process even simpler.
Reducers (Slices): How State Changes
Reducers are pure functions that take the current state and an action as arguments, and return a new state based on the action. They are the only way to change the state in a Redux application.
Key Characteristics of Reducers:
- Pure Functions:
- They always produce the same output given the same input arguments.
- They have no side effects (e.g., no API calls, no modifying external variables, no
console.loginside the core logic, no generating random numbers).
- No Direct State Mutation: Reducers must not directly modify the existing state object. Instead, they should return a new state object with the desired changes. (As we’ll see, Redux Toolkit’s Immer integration makes this easier by allowing “mutating” syntax that is safely translated into immutable updates.)
- Handle Unknown Actions: Reducers should always return the current state if they don’t recognize the action type.
Introducing Redux Slices with createSlice:
Redux Toolkit’s createSlice is a powerful API that combines the process of defining initial state, actions, and reducers for a specific “slice” of your Redux state. This dramatically reduces boilerplate.
Let’s create our first slice for a simple counter:
Create a
counterSlice.jsfile: Inside yoursrcfolder, create a new folder namedfeatures, and then acounterfolder inside it. Finally, createcounterSlice.jsinsidesrc/features/counter/counterSlice.js.// src/features/counter/counterSlice.js import { createSlice } from '@reduxjs/toolkit'; const initialState = { value: 0, }; export const counterSlice = createSlice({ name: 'counter', // This will be used as the prefix for action types (e.g., 'counter/increment') initialState, // The initial state for this slice reducers: { // Reducer functions go here. // Redux Toolkit uses Immer, so you can "mutate" state directly. increment: (state) => { state.value += 1; // Direct mutation is safe due to Immer }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { state.value += action.payload; // action.payload holds the value passed when dispatching }, }, }); // Action creators are generated for each case reducer function export const { increment, decrement, incrementByAmount } = counterSlice.actions; // The reducer for this slice export default counterSlice.reducer;Explanation:
createSlice: Takes an object withname,initialState, andreducers.name: A string that uniquely identifies this slice and is used as a prefix for the generated action types (e.g.,counter/increment).initialState: The initial state value for this particular slice of the Redux store.reducers: An object containing functions that define how the state changes in response to specific actions.- Inside these
reducerfunctions, you can write “mutating” logic directly (e.g.,state.value += 1). Redux Toolkit uses a library called Immer under the hood, which automatically handles the immutable updates for you, creating a new state object behind the scenes.
- Inside these
counterSlice.actions:createSliceautomatically generates action creator functions for each reducer function you define. These are exported for you to use when dispatching actions.counterSlice.reducer: This is the generated reducer function for this slice. You’ll add this to your main Redux store.
Add the Slice Reducer to the Store: Now, update your
src/app/store.jsto include thecounterReducer:// src/app/store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; // Import the slice reducer export const store = configureStore({ reducer: { // Register the counter reducer under the 'counter' key counter: counterReducer, }, });Now, your Redux store has a
counterslice, and its state can be accessed atstore.getState().counter.
Using Redux State and Actions in React Components
React-Redux provides hooks (useSelector and useDispatch) that allow your React components to easily interact with the Redux store.
useSelector: A hook that lets you extract data (or select a part of the state) from the Redux store. When the selected data changes, your component will re-render.useDispatch: A hook that gives you access to the Reddispatchfunction. You use this function to dispatch actions, which in turn trigger state updates in the Redux store.
Let’s create a Counter component that interacts with our Redux store:
Create a
Counter.jsxcomponent: Insidesrc/features/counter, createCounter.jsx.// src/features/counter/Counter.jsx import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { increment, decrement, incrementByAmount } from './counterSlice'; function Counter() { // Use useSelector to read the 'value' from the 'counter' slice of the Redux state const count = useSelector((state) => state.counter.value); // Use useDispatch to get the dispatch function const dispatch = useDispatch(); return ( <div style={{ textAlign: 'center', marginTop: '50px' }}> <h2>Counter: {count}</h2> <button onClick={() => dispatch(increment())} style={{ margin: '5px' }}> Increment </button> <button onClick={() => dispatch(decrement())} style={{ margin: '5px' }}> Decrement </button> <button onClick={() => dispatch(incrementByAmount(5))} style={{ margin: '5px' }}> Increment by 5 </button> </div> ); } export default Counter;Explanation:
useSelector((state) => state.counter.value): This “selects” thevalueproperty from thecounterslice of the Redux state. Wheneverstate.counter.valuechanges, this component will re-render to display the new value.useDispatch(): This hook provides thedispatchfunction.dispatch(increment()): When the “Increment” button is clicked, we dispatch theincrementaction (which was auto-generated bycreateSlice). This action will be sent to the Redux store, thecounterReducerwill process it, update the state, and then theCountercomponent will re-render.dispatch(incrementByAmount(5)): Demonstrates dispatching an action with a payload. The5is passed asaction.payloadto theincrementByAmountreducer.
Integrate
Counter.jsxinto yourApp.jsx:// src/App.jsx import React from 'react'; import Counter from './features/counter/Counter'; // Import the Counter component import './App.css'; // Assuming you have some basic styling function App() { return ( <div className="App"> <h1>Redux Counter App</h1> <Counter /> {/* Render the Counter component */} </div> ); } export default App;
Now, run npm run dev and you should see your Redux-powered counter application!
Exercises/Mini-Challenges:
Add a Reset Button:
- In
counterSlice.js, add a new reducer function calledresetthat sets thevalueback to0. - Export the
resetaction creator. - In
Counter.jsx, add a new button that dispatches theresetaction when clicked.
- In
Implement a Text Input for
incrementByAmount:- In
Counter.jsx, add aninputfield of typenumberand a local state variable (usinguseState) to manage its value. - When the “Increment by Amount” button is clicked, dispatch
incrementByAmountwith the value from the input field. Remember to convert the input value to a number.
- In
3. Intermediate Topics
Building upon the core concepts, let’s explore more intermediate topics that enhance your Redux application.
Managing Multiple Slices and combineReducers
In a real-world application, you’ll likely have many different pieces of state to manage (e.g., user authentication, a list of products, a shopping cart, UI themes, etc.). It’s a best practice to organize your Redux state into independent “slices,” each managed by its own createSlice and corresponding reducer.
When you have multiple slice reducers, configureStore automatically calls combineReducers for you if you provide an object to its reducer property. This function from the core Redux library helps you combine these smaller, independent reducers into a single “root reducer” that configureStore can understand.
Let’s add a user slice to manage user authentication state.
Create a
userSlice.jsfile:src/features/user/userSlice.js// src/features/user/userSlice.js import { createSlice } from '@reduxjs/toolkit'; const initialState = { username: '', isLoggedIn: false, }; export const userSlice = createSlice({ name: 'user', initialState, reducers: { login: (state, action) => { state.username = action.payload; state.isLoggedIn = true; }, logout: (state) => { state.username = ''; state.isLoggedIn = false; }, }, }); export const { login, logout } = userSlice.actions; export default userSlice.reducer;Combine Reducers in
store.js: Now, updatesrc/app/store.jsto include bothcounterReduceranduserReducer:// src/app/store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; import userReducer from '../features/user/userSlice'; // Import the user slice reducer export const store = configureStore({ reducer: { // Each key here represents a "slice" of your global Redux state counter: counterReducer, user: userReducer, // Add the user reducer }, });Now, your Redux state will look something like this:
{ counter: { value: 0 }, user: { username: '', isLoggedIn: false } }Create a
UserProfilecomponent to use theuserslice:src/features/user/UserProfile.jsx// src/features/user/UserProfile.jsx import React, { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { login, logout } from './userSlice'; function UserProfile() { const username = useSelector((state) => state.user.username); const isLoggedIn = useSelector((state) => state.user.isLoggedIn); const dispatch = useDispatch(); const [inputUsername, setInputUsername] = useState(''); const handleLogin = () => { if (inputUsername) { dispatch(login(inputUsername)); setInputUsername(''); // Clear input after login } }; const handleLogout = () => { dispatch(logout()); }; return ( <div style={{ textAlign: 'center', marginTop: '30px', borderTop: '1px solid #eee', paddingTop: '20px' }}> <h3>User Profile</h3> {isLoggedIn ? ( <> <p>Welcome, {username}!</p> <button onClick={handleLogout}>Logout</button> </> ) : ( <> <input type="text" value={inputUsername} onChange={(e) => setInputUsername(e.target.value)} placeholder="Enter username" style={{ margin: '5px', padding: '8px' }} /> <button onClick={handleLogin}>Login</button> </> )} </div> ); } export default UserProfile;Add
UserProfiletoApp.jsx:// src/App.jsx import React from 'react'; import Counter from './features/counter/Counter'; import UserProfile from './features/user/UserProfile'; // Import UserProfile import './App.css'; function App() { return ( <div className="App"> <h1>Redux Multi-Slice App</h1> <Counter /> <UserProfile /> {/* Render UserProfile */} </div> ); } export default App;
Asynchronous Logic with createAsyncThunk
Reducers must be pure and synchronous. This means they cannot directly handle side effects like API calls, timers, or database interactions. For asynchronous logic, Redux uses middleware. Redux Toolkit provides createAsyncThunk, which is the recommended way to handle async operations in Redux.
createAsyncThunk generates “thunks” (functions that return another function) that dispatch actions for the different stages of an asynchronous operation:
pending: when the async operation starts.fulfilled: when the async operation succeeds.rejected: when the async operation fails.
Let’s demonstrate fetching a list of “posts” from a public API using createAsyncThunk.
Create a
postsSlice.jsfile:src/features/posts/postsSlice.js// src/features/posts/postsSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // Define the initial state for our posts slice const initialState = { posts: [], status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' error: null, }; // createAsyncThunk for fetching posts // The first argument is the action type prefix // The second argument is an async function that returns a Promise export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => { const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5'); const data = await response.json(); return data; // This value becomes the `action.payload` for the 'fulfilled' action }); export const postsSlice = createSlice({ name: 'posts', initialState, reducers: { // Standard reducers for synchronous actions (not needed for this example, but good to know) // For example: // addPost: (state, action) => { // state.posts.push(action.payload); // } }, // extraReducers handle actions from other slices or createAsyncThunk extraReducers: (builder) => { builder .addCase(fetchPosts.pending, (state) => { state.status = 'loading'; state.error = null; // Clear previous errors }) .addCase(fetchPosts.fulfilled, (state, action) => { state.status = 'succeeded'; state.posts = action.payload; // Store the fetched posts }) .addCase(fetchPosts.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; // Store the error message }); }, }); // We don't need to export actions from `postsSlice.actions` for async thunks // because `createAsyncThunk` handles generating and dispatching those actions internally. export default postsSlice.reducer;Explanation:
createAsyncThunk('posts/fetchPosts', async () => { ... }):'posts/fetchPosts'is the action type prefix.createAsyncThunkwill generateposts/fetchPosts/pending,posts/fetchPosts/fulfilled, andposts/fetchPosts/rejectedaction types.- The async function performs the data fetching. Whatever it
returns will become thepayloadof thefulfilledaction. If it throws an error, the error will be in therejectedaction.
extraReducers: This is a special property increateSlicethat allows your slice to respond to actions not defined directly within itsreducersobject. This is essential forcreateAsyncThunkbecause the thunk dispatches actions with types thatcreateSlicedoesn’t know about directly.builder.addCase(fetchPosts.pending, ...): Handles thependingstate of the async operation.builder.addCase(fetchPosts.fulfilled, ...): Handles thefulfilled(success) state, where you update your state with the fetched data.builder.addCase(fetchPosts.rejected, ...): Handles therejected(failure) state, typically storing an error message.
Add the
postsReducertostore.js:// src/app/store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; import userReducer from '../features/user/userSlice'; import postsReducer from '../features/posts/postsSlice'; // Import posts reducer export const store = configureStore({ reducer: { counter: counterReducer, user: userReducer, posts: postsReducer, // Add the posts reducer }, });Create a
PostsListcomponent to display the fetched data:src/features/posts/PostsList.jsx// src/features/posts/PostsList.jsx import React, { useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { fetchPosts } from './postsSlice'; function PostsList() { const posts = useSelector((state) => state.posts.posts); const postStatus = useSelector((state) => state.posts.status); const error = useSelector((state) => state.posts.error); const dispatch = useDispatch(); // Dispatch the thunk when the component mounts // This is a common pattern, but be aware of its implications for SSR/prefetching (see Advanced Topics) useEffect(() => { if (postStatus === 'idle') { dispatch(fetchPosts()); } }, [postStatus, dispatch]); // Re-run effect if status or dispatch changes let content; if (postStatus === 'loading') { content = <p>Loading posts...</p>; } else if (postStatus === 'succeeded') { content = posts.map((post) => ( <div key={post.id} style={{ border: '1px solid #ccc', margin: '10px', padding: '10px', borderRadius: '5px' }}> <h3>{post.title}</h3> <p>{post.body.substring(0, 100)}...</p> {/* Show first 100 chars */} </div> )); } else if (postStatus === 'failed') { content = <p>Error: {error}</p>; } return ( <div style={{ textAlign: 'center', marginTop: '30px', borderTop: '1px solid #eee', paddingTop: '20px' }}> <h2>Posts</h2> {content} </div> ); } export default PostsList;Add
PostsListtoApp.jsx:// src/App.jsx import React from 'react'; import Counter from './features/counter/Counter'; import UserProfile from './features/user/UserProfile'; import PostsList from './features/posts/PostsList'; // Import PostsList import './App.css'; function App() { return ( <div className="App"> <h1>Redux App with Multiple Slices & Async Data</h1> <Counter /> <UserProfile /> <PostsList /> {/* Render PostsList */} </div> ); } export default App;
Exercises/Mini-Challenges:
Clear Posts Button:
- In
postsSlice.js, add a synchronous reducer calledclearPoststhat sets thepostsarray to empty andstatustoidle. - Export the
clearPostsaction creator. - In
PostsList.jsx, add a button that dispatchesclearPosts.
- In
Display Loading Spinner (Advanced):
- Replace the “Loading posts…” text in
PostsList.jsxwith a simple CSS spinner or a GIF. You can find simple spinner CSS examples online.
- Replace the “Loading posts…” text in
4. Advanced Topics and Best Practices
As your Redux application grows, you’ll encounter more complex scenarios. Here are some advanced topics and best practices to ensure your Redux code remains maintainable, performant, and robust.
Typed Hooks with TypeScript
If you’re using TypeScript (which is highly recommended for larger React/Redux applications), you’ll want to create “typed” versions of useSelector and useDispatch to get full type safety and auto-completion benefits.
Create a
hooks.js(orhooks.tsfor TypeScript) file:src/app/hooks.js(orsrc/app/hooks.ts)// src/app/hooks.js (For JavaScript projects) // For TypeScript, you would use src/app/hooks.ts as shown below import { useDispatch, useSelector } from 'react-redux'; // This is for demonstration. For actual type safety, use the TypeScript version below. export const useAppDispatch = () => useDispatch(); export const useAppSelector = useSelector;// src/app/hooks.ts (Recommended for TypeScript projects) import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from './store'; // Import types from your store // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;Use your typed hooks in components: Replace
useSelectoranduseDispatchimports in your components (Counter.jsx,UserProfile.jsx,PostsList.jsx) with your new typed hooks.For example, in
Counter.jsx:// src/features/counter/Counter.jsx import React from 'react'; import { useAppSelector, useAppDispatch } from '../../app/hooks'; // Adjust path import { increment, decrement, incrementByAmount } from './counterSlice'; function Counter() { const count = useAppSelector((state) => state.counter.value); const dispatch = useAppDispatch(); // ... rest of your component }
This provides compile-time checks and excellent IDE support, catching errors before you even run your code.
Normalizing State
When dealing with large collections of data, especially data fetched from APIs that have nested or relational structures, it’s a best practice to store that data in a “normalized” form in your Redux store.
What is Normalized State?
- Each item (entity) is stored once, by its ID.
- “Relationships” between entities are stored by referencing IDs.
- Data is typically stored in an object with
ids as keys and the entities themselves as values, along with an array ofallIdsfor ordering or iteration.
Benefits of Normalization:
- Easier Updates: You only need to update an item in one place, no matter where it’s referenced in your UI.
- Improved Performance: Prevents unnecessary re-renders when only a small part of a nested object changes.
- Simpler Selectors: Easier to retrieve specific items by ID.
Redux Toolkit provides createEntityAdapter to help manage normalized state. It generates a set of pre-built reducers and selectors for common CRUD (Create, Read, Update, Delete) operations on normalized data.
Let’s refactor our postsSlice to use createEntityAdapter.
Update
postsSlice.js:// src/features/posts/postsSlice.js import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'; // Create an entity adapter for posts // It provides pre-built reducers and selectors for CRUD operations const postsAdapter = createEntityAdapter({ sortComparer: (a, b) => b.id - a.id, // Example: sort posts by ID in descending order }); // Define the initial state using the adapter's getInitialState // This will create { ids: [], entities: {} } const initialState = postsAdapter.getInitialState({ status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' error: null, }); // createAsyncThunk for fetching posts (same as before) export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => { const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5'); const data = await response.json(); return data; }); export const postsSlice = createSlice({ name: 'posts', initialState, reducers: { // Example of adding a new post manually (if needed) postAdded: postsAdapter.addOne, // Automatically adds an entity // Example of updating a post postUpdated: postsAdapter.updateOne, // Automatically updates an entity }, extraReducers: (builder) => { builder .addCase(fetchPosts.pending, (state) => { state.status = 'loading'; }) .addCase(fetchPosts.fulfilled, (state, action) => { state.status = 'succeeded'; // Use the adapter to set all fetched posts postsAdapter.setAll(state, action.payload); }) .addCase(fetchPosts.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); }, }); // Export the auto-generated action creators from the slice's reducers export const { postAdded, postUpdated } = postsSlice.actions; // Export the adapter's selectors. // getSelectors creates a set of memoized selectors (more on this in the next section) export const { selectAll: selectAllPosts, selectById: selectPostById, selectIds: selectPostIds, } = postsAdapter.getSelectors((state) => state.posts); // Tell selectors where to find the posts state export default postsSlice.reducer;Update
PostsList.jsxto use the new selectors:// src/features/posts/PostsList.jsx import React, { useEffect } from 'react'; import { useAppSelector, useAppDispatch } from '../../app/hooks'; // Use typed hooks import { fetchPosts, selectAllPosts } from './postsSlice'; // Import selectAllPosts function PostsList() { // Use the new selector to get all posts const posts = useAppSelector(selectAllPosts); const postStatus = useAppSelector((state) => state.posts.status); const error = useAppSelector((state) => state.posts.error); const dispatch = useAppDispatch(); useEffect(() => { if (postStatus === 'idle') { dispatch(fetchPosts()); } }, [postStatus, dispatch]); let content; if (postStatus === 'loading') { content = <p>Loading posts...</p>; } else if (postStatus === 'succeeded') { content = posts.map((post) => ( <div key={post.id} style={{ border: '1px solid #ccc', margin: '10px', padding: '10px', borderRadius: '5px' }}> <h3>{post.title}</h3> <p>{post.body.substring(0, 100)}...</p> </div> )); } else if (postStatus === 'failed') { content = <p>Error: {error}</p>; } return ( <div style={{ textAlign: 'center', marginTop: '30px', borderTop: '1px solid #eee', paddingTop: '20px' }}> <h2>Posts</h2> {content} </div> ); } export default PostsList;
Memoized Selectors with Reselect
useSelector can sometimes lead to unnecessary re-renders if the selected data is derived or computed in the selector function itself, and that computation returns a new object reference on every render, even if the underlying data hasn’t logically changed.
Memoized selectors help solve this. They are functions that remember the last inputs and the last output. If the inputs are the same, they return the cached output instead of re-computing, preventing unnecessary re-renders.
createEntityAdapter’s getSelectors function already provides memoized selectors (e.g., selectAllPosts, selectPostById). For more complex derived data, you can use the createSelector utility from Redux Toolkit (which re-exports Reselect).
Example of a custom memoized selector (if you weren’t using createEntityAdapter for selectAllPosts):
// In postsSlice.js or a separate selectors.js file
import { createSelector } from '@reduxjs/toolkit';
// A simple input selector that returns the posts slice of the state
const selectPostsState = (state) => state.posts;
// A memoized selector to get all posts (if you weren't using createEntityAdapter)
export const selectAllPostsCustom = createSelector(
[selectPostsState], // Array of input selectors
(postsState) => Object.values(postsState.entities) // Transformation function
);
// Another example: Select posts that have a specific keyword in their title
export const selectPostsByKeyword = createSelector(
[selectAllPosts, (state, keyword) => keyword], // Inputs: all posts and the keyword argument
(posts, keyword) => posts.filter(post => post.title.includes(keyword))
);
Using createSelector ensures that selectPostsByKeyword only re-runs its filtering logic if posts or keyword actually change.
Common Pitfalls and Best Practices
- Avoid Accidental Mutations: Although Redux Toolkit uses Immer to simplify immutable updates, it’s crucial to understand the concept. If you manually modify state outside of a Redux Toolkit reducer (e.g., in a
useEffector a component’suseState), Redux won’t detect the change, leading to unexpected behavior.configureStoreincludes a middleware that helps catch these mutations in development. - Keep Slices Small and Focused: Each slice (
counterSlice,userSlice,postsSlice) should manage a related piece of state for a specific feature or domain. This promotes modularity and maintainability. - Organize Files by Feature (Ducks Pattern): Instead of separate
actionsandreducersfolders, a common and recommended practice is to put all Redux logic (slice definition, async thunks, selectors) for a feature into a single file within a “feature folder” (e.g.,src/features/counter/counterSlice.js). This makes it easier to find and work with related code. - “Ducks Pattern” for File Structure:
src/ ├── app/ │ ├── store.js │ └── hooks.js (or .ts) ├── features/ │ ├── counter/ │ │ ├── counterSlice.js │ │ └── Counter.jsx │ ├── user/ │ │ ├── userSlice.js │ │ └── UserProfile.jsx │ └── posts/ │ ├── postsSlice.js │ └── PostsList.jsx └── App.jsx └── main.jsx - Handle Loading and Error States in Slices: As demonstrated with
postsSlice, it’s good practice to managestatus(idle,loading,succeeded,failed) anderrorstates directly within the relevant slice. This centralizes data fetching status and makes it easy for any component to access. - Redux DevTools: Always install and use the Redux DevTools Extension. It’s an invaluable tool for understanding your application’s state flow, debugging, and time-traveling through state changes.
configureStoreautomatically enables it. - Don’t Fetch Data in
useEffectfor Render-Critical Data (Prefer RTK Query or Server Components): While we useduseEffectto triggerfetchPostsfor simplicity, for complex applications, especially those requiring Server-Side Rendering (SSR) or advanced caching, consider using:- RTK Query: Part of Redux Toolkit, it’s a powerful data fetching and caching library that handles most of the boilerplate for you (loading states, caching, invalidation, retries). It’s generally preferred for managing server state.
- React 19’s Server Components/Actions: For Next.js or Remix applications, these new React features allow data fetching to happen on the server before components render on the client, avoiding waterfall issues and providing better initial load performance.
- The
useEffectapproach for data fetching, while functional, can lead to issues like late data availability, manual state management for loading/error, and difficulties with SSR. It’s often considered an anti-pattern for “render-critical data.”
5. Guided Projects
Let’s apply what we’ve learned by building two guided projects.
Project 1: Simple Todo Application
Objective: Build a simple Todo application where users can add and toggle the completion status of todos. This project will reinforce the concepts of createSlice, useSelector, and useDispatch.
Steps:
Create the
todosSlice:- Create
src/features/todos/todosSlice.js. - Define an
initialStatefor an array of todo objects, where each todo hasid,text, andcompletedproperties. - Add a
todoAddedreducer that takes apayload(the todo text) and adds a new todo object to thestatearray with a uniqueid(e.g., usingDate.now()). - Add a
todoToggledreducer that takes anidaspayloadand toggles thecompletedstatus of the matching todo. - Export the action creators and the reducer.
// src/features/todos/todosSlice.js import { createSlice } from '@reduxjs/toolkit'; const initialState = []; export const todosSlice = createSlice({ name: 'todos', initialState, reducers: { todoAdded: (state, action) => { // Payload is the text of the todo state.push({ id: Date.now(), // Simple unique ID text: action.payload, completed: false, }); }, todoToggled: (state, action) => { // Payload is the ID of the todo to toggle const todo = state.find((t) => t.id === action.payload); if (todo) { todo.completed = !todo.completed; } }, // Mini-Challenge: Add a reducer to remove a todo todoRemoved: (state, action) => { return state.filter(todo => todo.id !== action.payload); }, }, }); export const { todoAdded, todoToggled, todoRemoved } = todosSlice.actions; export default todosSlice.reducer;- Create
Add
todosReducertostore.js:// src/app/store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; import userReducer from '../features/user/userSlice'; import postsReducer from '../features/posts/postsSlice'; import todosReducer from '../features/todos/todosSlice'; // Import todos reducer export const store = configureStore({ reducer: { counter: counterReducer, user: userReducer, posts: postsReducer, todos: todosReducer, // Add the todos reducer }, });Create
TodosListComponent:- Create
src/features/todos/TodosList.jsx. - Use
useAppSelectorto get thetodosarray from the state. - Use
useAppDispatchto dispatch actions. - Render an input field and a button to add new todos.
- Map over the
todosarray and display each todo. - For each todo, add a checkbox to toggle its completion status and display its text. Style completed todos differently (e.g., strikethrough).
// src/features/todos/TodosList.jsx import React, { useState } from 'react'; import { useAppSelector, useAppDispatch } from '../../app/hooks'; import { todoAdded, todoToggled, todoRemoved } from './todosSlice'; // Import actions function TodosList() { const todos = useAppSelector((state) => state.todos); const dispatch = useAppDispatch(); const [newTodoText, setNewTodoText] = useState(''); const handleAddTodo = () => { if (newTodoText.trim()) { dispatch(todoAdded(newTodoText)); setNewTodoText(''); } }; return ( <div style={{ textAlign: 'center', marginTop: '30px', borderTop: '1px solid #eee', paddingTop: '20px' }}> <h2>My Todos</h2> <div> <input type="text" value={newTodoText} onChange={(e) => setNewTodoText(e.target.value)} placeholder="Add a new todo" style={{ padding: '8px', margin: '5px' }} /> <button onClick={handleAddTodo}>Add Todo</button> </div> <ul style={{ listStyle: 'none', padding: 0 }}> {todos.map((todo) => ( <li key={todo.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '8px', borderBottom: '1px dotted #ccc', textDecoration: todo.completed ? 'line-through' : 'none', color: todo.completed ? '#888' : '#333', }} > <input type="checkbox" checked={todo.completed} onChange={() => dispatch(todoToggled(todo.id))} style={{ marginRight: '10px' }} /> <span>{todo.text}</span> {/* Mini-Challenge: Add a delete button */} <button onClick={() => dispatch(todoRemoved(todo.id))} style={{ marginLeft: '10px', background: 'red', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer' }}> X </button> </li> ))} </ul> </div> ); } export default TodosList;- Create
Add
TodosListtoApp.jsx:// src/App.jsx import React from 'react'; import Counter from './features/counter/Counter'; import UserProfile from './features/user/UserProfile'; import PostsList from './features/posts/PostsList'; import TodosList from './features/todos/TodosList'; // Import TodosList import './App.css'; function App() { return ( <div className="App"> <h1>Redux App with Multiple Features</h1> <Counter /> <UserProfile /> <PostsList /> <TodosList /> {/* Render TodosList */} </div> ); } export default App;
Encourage Independent Problem-Solving:
- Mini-Challenge: Implement a “Clear Completed” button that removes all
completedtodos from the state. You’ll need a new reducer intodosSlice.js. - Mini-Challenge: Add a “Filter” option (e.g., “All”, “Active”, “Completed”) to the Todo app. This will require a new
filterSliceand combining it into yourstore.js. Then, updateTodosList.jsxto display only the relevant todos based on the filter.
Project 2: Basic E-commerce Cart
Objective: Build a simplified shopping cart functionality where users can add items, remove items, and adjust quantities. This project will use createEntityAdapter for managing cart items and createAsyncThunk for a simulated “checkout” process.
Steps:
Create the
cartSlicewithcreateEntityAdapter:- Create
src/features/cart/cartSlice.js. - Use
createEntityAdapterto managecartItems. Each item will haveid,name,price, andquantity. - Define reducers:
addItem: Adds a new item or increments quantity if it exists.removeItem: Removes an item.updateQuantity: Adjusts an item’s quantity.
- Define an async thunk
checkoutCartthat simulates an API call (e.g., usingsetTimeoutfor a delay). This thunk should dispatchpending,fulfilled, andrejectedactions. - In
extraReducers, handle the lifecycle actions ofcheckoutCartto manage loading status and clear the cart on success.
// src/features/cart/cartSlice.js import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'; // Define the shape of a cart item // type CartItem = { id: string | number; name: string; price: number; quantity: number; } const cartAdapter = createEntityAdapter(); // No custom selectId or sortComparer needed for this basic example const initialState = cartAdapter.getInitialState({ checkoutStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' checkoutError: null, }); // Async thunk for simulated checkout export const checkoutCart = createAsyncThunk( 'cart/checkout', async (_, { getState, rejectWithValue }) => { // Get current cart items from state const cartItems = selectAllCartItems(getState()); if (cartItems.length === 0) { return rejectWithValue('Cart is empty!'); } try { // Simulate API call await new Promise((resolve) => setTimeout(resolve, 1500)); // In a real app, you'd send `cartItems` to a backend here console.log('Checking out cart:', cartItems); return 'Order placed successfully!'; // Payload for fulfilled } catch (error) { return rejectWithValue(error.message || 'Failed to checkout'); // Payload for rejected } } ); export const cartSlice = createSlice({ name: 'cart', initialState, reducers: { addItem: (state, action) => { const { id, name, price } = action.payload; const existingItem = state.entities[id]; if (existingItem) { existingItem.quantity += 1; } else { cartAdapter.addOne(state, { id, name, price, quantity: 1 }); } }, removeItem: cartAdapter.removeOne, updateQuantity: (state, action) => { const { id, quantity } = action.payload; if (quantity <= 0) { cartAdapter.removeOne(state, id); } else { cartAdapter.updateOne(state, { id, changes: { quantity } }); } }, clearCart: cartAdapter.removeAll, // Clear all items }, extraReducers: (builder) => { builder .addCase(checkoutCart.pending, (state) => { state.checkoutStatus = 'loading'; state.checkoutError = null; }) .addCase(checkoutCart.fulfilled, (state, action) => { state.checkoutStatus = 'succeeded'; state.checkoutError = null; cartAdapter.removeAll(state); // Clear cart after successful checkout console.log(action.payload); // Log success message }) .addCase(checkoutCart.rejected, (state, action) => { state.checkoutStatus = 'failed'; state.checkoutError = action.payload; // Set error message console.error('Checkout failed:', action.payload); }); }, }); export const { addItem, removeItem, updateQuantity, clearCart } = cartSlice.actions; export const { selectAll: selectAllCartItems, selectById: selectCartItemById, selectIds: selectCartItemIds, } = cartAdapter.getSelectors((state) => state.cart); export default cartSlice.reducer;- Create
Add
cartReducertostore.js:// src/app/store.js import { configureStore } from '@reduxjs/toolkit'; import counterReducer from '../features/counter/counterSlice'; import userReducer from '../features/user/userSlice'; import postsReducer from '../features/posts/postsSlice'; import todosReducer from '../features/todos/todosSlice'; import cartReducer from '../features/cart/cartSlice'; // Import cart reducer export const store = configureStore({ reducer: { counter: counterReducer, user: userReducer, posts: postsReducer, todos: todosReducer, cart: cartReducer, // Add the cart reducer }, });Create a
ProductListComponent (for adding items):- Create
src/features/cart/ProductList.jsx. - Display a few dummy products with “Add to Cart” buttons.
// src/features/cart/ProductList.jsx import React from 'react'; import { useAppDispatch } from '../../app/hooks'; import { addItem } from './cartSlice'; const products = [ { id: 'p1', name: 'Laptop', price: 1200 }, { id: 'p2', name: 'Mouse', price: 25 }, { id: 'p3', name: 'Keyboard', price: 75 }, ]; function ProductList() { const dispatch = useAppDispatch(); return ( <div style={{ textAlign: 'center', marginTop: '30px', borderTop: '1px solid #eee', paddingTop: '20px' }}> <h2>Available Products</h2> <div style={{ display: 'flex', justifyContent: 'center', gap: '20px', flexWrap: 'wrap' }}> {products.map((product) => ( <div key={product.id} style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px', width: '200px', backgroundColor: '#f9f9f9', }} > <h4>{product.name}</h4> <p>${product.price.toFixed(2)}</p> <button onClick={() => dispatch(addItem(product))}>Add to Cart</button> </div> ))} </div> </div> ); } export default ProductList;- Create
Create a
ShoppingCartComponent:- Create
src/features/cart/ShoppingCart.jsx. - Use
selectAllCartItemsto display items in the cart. - For each item, show name, price, quantity, and total.
- Add buttons to increase/decrease quantity and remove items.
- Add a “Checkout” button that dispatches
checkoutCart. DisplaycheckoutStatusandcheckoutError.
// src/features/cart/ShoppingCart.jsx import React from 'react'; import { useAppSelector, useAppDispatch } from '../../app/hooks'; import { selectAllCartItems, updateQuantity, removeItem, checkoutCart, clearCart } from './cartSlice'; function ShoppingCart() { const cartItems = useAppSelector(selectAllCartItems); const checkoutStatus = useAppSelector((state) => state.cart.checkoutStatus); const checkoutError = useAppSelector((state) => state.cart.checkoutError); const dispatch = useAppDispatch(); const totalAmount = cartItems.reduce((total, item) => total + item.price * item.quantity, 0); const handleQuantityChange = (id, newQuantity) => { dispatch(updateQuantity({ id, quantity: newQuantity })); }; const handleRemoveItem = (id) => { dispatch(removeItem(id)); }; const handleCheckout = () => { dispatch(checkoutCart()); }; return ( <div style={{ textAlign: 'center', marginTop: '30px', borderTop: '1px solid #eee', paddingTop: '20px' }}> <h2>Shopping Cart</h2> {cartItems.length === 0 ? ( <p>Your cart is empty.</p> ) : ( <> <ul style={{ listStyle: 'none', padding: 0 }}> {cartItems.map((item) => ( <li key={item.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px', borderBottom: '1px solid #f0f0f0', maxWidth: '400px', margin: '0 auto', }} > <span>{item.name} (${item.price.toFixed(2)})</span> <div> <button onClick={() => handleQuantityChange(item.id, item.quantity - 1)}>-</button> <span style={{ margin: '0 10px' }}>{item.quantity}</span> <button onClick={() => handleQuantityChange(item.id, item.quantity + 1)}>+</button> <button onClick={() => handleRemoveItem(item.id)} style={{ marginLeft: '10px', background: 'red', color: 'white', border: 'none', borderRadius: '3px', cursor: 'pointer' }}> Remove </button> </div> </li> ))} </ul> <h3 style={{ marginTop: '20px' }}>Total: ${totalAmount.toFixed(2)}</h3> <div> <button onClick={handleCheckout} disabled={checkoutStatus === 'loading'} style={{ margin: '5px', padding: '10px 20px' }}> {checkoutStatus === 'loading' ? 'Processing...' : 'Checkout'} </button> <button onClick={() => dispatch(clearCart())} style={{ margin: '5px', padding: '10px 20px', background: '#f44336', color: 'white' }}> Clear Cart </button> </div> {checkoutStatus === 'failed' && <p style={{ color: 'red' }}>Error: {checkoutError}</p>} {checkoutStatus === 'succeeded' && <p style={{ color: 'green' }}>Checkout successful!</p>} </> )} </div> ); } export default ShoppingCart;- Create
Add
ProductListandShoppingCarttoApp.jsx:// src/App.jsx import React from 'react'; import Counter from './features/counter/Counter'; import UserProfile from './features/user/UserProfile'; import PostsList from './features/posts/PostsList'; import TodosList from './features/todos/TodosList'; import ProductList from './features/cart/ProductList'; // Import ProductList import ShoppingCart from './features/cart/ShoppingCart'; // Import ShoppingCart import './App.css'; function App() { return ( <div className="App"> <h1>Redux E-commerce Demo</h1> <Counter /> <UserProfile /> <PostsList /> <TodosList /> <ProductList /> {/* Render ProductList */} <ShoppingCart /> {/* Render ShoppingCart */} </div> ); } export default App;
Encourage Independent Problem-Solving:
- Mini-Challenge: Display a success message after checkout and then clear the message after a few seconds.
- Mini-Challenge: Implement a loading indicator for the “Add to Cart” button (though less common for synchronous Redux actions, you could simulate it with a very quick
setTimeoutif desired). - Mini-Challenge: Add a “Toast” notification system using Redux for displaying success/error messages globally. This would involve a new
notificationsSlice.
6. Bonus Section: Further Learning and Resources
Congratulations on completing this comprehensive guide to Redux for React! You now have a solid foundation for building scalable and maintainable applications. The journey doesn’t stop here, though. Here are some excellent resources to continue your learning:
Recommended Online Courses/Tutorials:
- Redux Essentials Tutorial (Official): This is the official “how-to” guide that uses Redux Toolkit.
- Redux Fundamentals Tutorial (Official): This “bottom-up” tutorial explains how Redux works from first principles.
- Learn Modern Redux - Redux Toolkit, React-Redux Hooks, and RTK Query (by Mark Erikson): A livestream episode covering modern Redux patterns with a live-coded example.
- Redux for Beginners - The Brain-Friendly Guide to Learning Redux (freeCodeCamp): An easy-to-follow tutorial building a small todo app with Redux Toolkit.
- Getting Started with Redux - Video Series (by Dan Abramov on Egghead.io): The original video series by Redux creator Dan Abramov, demonstrating core concepts (older but still valuable for understanding principles).
Official Documentation:
- Redux.js.org (Official Redux Documentation): The ultimate source for all things Redux. Contains tutorials, API references, and style guides.
- Redux Toolkit (Official Documentation): Detailed documentation for all Redux Toolkit APIs and features.
- React Redux (Official Documentation): Documentation for the official React bindings for Redux.
- Immer.js Documentation: Learn more about the library Redux Toolkit uses for “mutating” updates.
Blogs and Articles:
- Redux Style Guide: Official recommendations on how to write Redux code, including best practices for structuring files and naming conventions.
- Mark Erikson’s Blog (Redux Co-maintainer): In-depth articles on various Redux and React topics, including advanced concepts and common pitfalls.
- Medium.com & Dev.to: Search for “Redux React tutorial 2025” or “Redux Toolkit best practices” on these platforms for community-written articles and fresh perspectives.
YouTube Channels:
- Traversy Media: Often has beginner-friendly tutorials on React and Redux.
- Academind: Provides comprehensive crash courses on modern web technologies.
- freeCodeCamp.org: Their channel features many full-length courses and tutorials.
- MohitDecodes: Look for their “React 19 Features You Should Know in 2025” for broader React context.
- Source Coder Hub: Look for “React Redux Tutorial 2025” series for practical guides.
Community Forums/Groups:
- Stack Overflow: A vast resource for answers to specific programming questions. Use the
redux,react-redux, andredux-toolkittags. - Redux GitHub Discussions: Engage directly with the Redux community and maintainers.
- Reactiflux Discord Server: A large Discord community for React, Redux, and related technologies.
Next Steps/Advanced Topics:
- RTK Query: Dive deeper into RTK Query for comprehensive data fetching and caching, especially for server-side data. This is a powerful part of Redux Toolkit that handles a lot of complexity for you.
- Redux Persist: Learn how to persist your Redux state to local storage so that it survives page refreshes.
- Middleware Patterns: Explore other Redux middleware besides
redux-thunk, such asredux-sagaorcreateListenerMiddleware(from RTK), for more complex side effect management. - Advanced Selector Patterns: Master the use of
createSelectorfor performance optimization and deriving complex data. - Testing Redux Applications: Learn how to write unit and integration tests for your Redux reducers, actions, and components.
- Server-Side Rendering (SSR) with Redux: Understand how to integrate Redux with SSR frameworks like Next.js or Remix for better performance and SEO.
- Integration with other State Management Libraries: Understand how Redux (for global client state) often works in tandem with libraries like React Query (for server state) in modern applications.
Keep building, experimenting, and exploring! Redux is a powerful tool, and with practice, you’ll become proficient in managing complex application states effectively.