How to Implement Redux

How to Implement Redux Redux is a predictable state container for JavaScript applications, designed to help manage global state in a consistent and scalable way. Originally developed for React applications, Redux has since become a widely adopted pattern across frameworks like Angular, Vue, and even vanilla JavaScript. Its core strength lies in enforcing a unidirectional data flow, making state ch

Nov 10, 2025 - 08:30
Nov 10, 2025 - 08:30
 1

How to Implement Redux

Redux is a predictable state container for JavaScript applications, designed to help manage global state in a consistent and scalable way. Originally developed for React applications, Redux has since become a widely adopted pattern across frameworks like Angular, Vue, and even vanilla JavaScript. Its core strength lies in enforcing a unidirectional data flow, making state changes transparent, traceable, and testable. Whether you're building a complex e-commerce platform, a real-time dashboard, or a multi-page application with shared data, implementing Redux correctly can dramatically improve maintainability and performance.

Many developers initially find Redux intimidating due to its boilerplate and conceptual overhead. However, once understood, its structure becomes a powerful ally in managing complex UI logic. This guide provides a comprehensive, step-by-step walkthrough on how to implement Redux from scratch, including modern best practices, essential tools, real-world examples, and answers to common questions. By the end of this tutorial, youll have the knowledge and confidence to integrate Redux into your next projectefficiently, correctly, and with full understanding of why each step matters.

Step-by-Step Guide

Step 1: Understand Reduxs Core Concepts

Before writing a single line of code, its critical to understand the three foundational principles of Redux:

  • Single Source of Truth: The entire state of your application is stored in a single JavaScript object called the store.
  • State is Read-Only: The only way to change the state is by emitting an actionan object describing what happened.
  • Changes are Made with Pure Functions: Reducers are pure functions that take the previous state and an action, and return a new state.

These principles ensure that state changes are predictable and debuggable. Unlike direct state mutations (e.g., modifying a React components state with setState), Redux requires explicit actions to trigger changes, making it easier to track how and why state evolved over time.

Step 2: Set Up Your Project Environment

To implement Redux, youll need a JavaScript project. If youre starting from scratch, use a tool like Vite, Create React App (CRA), or Next.js. For this guide, well assume youre using React with Vite:

npm create vite@latest my-redux-app -- --template react

cd my-redux-app

npm install

Next, install the Redux libraries:

npm install @reduxjs/toolkit react-redux

@reduxjs/toolkit is the official, opinionated toolkit for Redux development. It simplifies store setup, action creation, and reducer logic by reducing boilerplate. react-redux provides the React bindings that connect your React components to the Redux store.

Step 3: Create the Redux Store

The store is the central hub of your Redux application. It holds the state, allows access via getState(), and dispatches actions via dispatch().

Create a new directory: src/store. Inside, create a file named store.js:

import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({

reducer: {},

});

export default store;

At this point, the store is empty because we havent added any reducers. Well fix that in the next step. The configureStore function from Redux Toolkit automatically sets up middleware (like Redux Thunk for async logic) and enables DevTools, so you dont need to configure them manually.

Step 4: Define Your First Reducer

Reducers are pure functions that determine how the state changes in response to actions. They must never mutate the existing statethey must return a new state object.

Lets create a simple counter feature. Inside src/store, create a new file: counterSlice.js:

import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({

name: 'counter',

initialState: {

value: 0,

},

reducers: {

increment: (state) => {

state.value += 1;

},

decrement: (state) => {

state.value -= 1;

},

reset: (state) => {

state.value = 0;

},

},

});

export const { increment, decrement, reset } = counterSlice.actions;

export default counterSlice.reducer;

Here, createSlice automatically generates action creators and action types based on the reducers you define. The syntax inside each reducer looks like its mutating state, but Redux Toolkit uses Immer under the hood to safely produce immutable updates. This eliminates the need for manual spread operators and makes reducers far more readable.

Step 5: Combine Reducers and Update the Store

Most applications have multiple slices of state. Combine them using combineReducers (or let Redux Toolkit handle it automatically).

Go back to src/store/store.js and import your counter reducer:

import { configureStore } from '@reduxjs/toolkit';

import counterReducer from './counterSlice';

const store = configureStore({

reducer: {

counter: counterReducer,

},

});

export default store;

Now your store has one slice: counter. The state structure will look like this:

{

counter: {

value: 0

}

}

Step 6: Wrap Your App with the Provider

To make the Redux store available to all components in your React app, you must wrap your root component with the Provider component from react-redux.

Open src/main.jsx (or src/main.js if using CRA) and update it:

import React from 'react'

import ReactDOM from 'react-dom/client'

import App from './App.jsx'

import './index.css'

import store from './store/store'

ReactDOM.createRoot(document.getElementById('root')).render(

<React.StrictMode>

<Provider store={store}>

<App />

</Provider>

</React.StrictMode>

)

Now, any component inside <App /> can access the Redux store.

Step 7: Connect Components to the Store

To read state from the store or dispatch actions, use the hooks provided by react-redux: useSelector and useDispatch.

Create a new component: src/components/Counter.jsx:

import React from 'react';

import { useSelector, useDispatch } from 'react-redux';

import { increment, decrement, reset } from '../store/counterSlice';

const Counter = () => {

const count = useSelector(state => state.counter.value);

const dispatch = useDispatch();

return (

<div>

<h3>Count: {count}</h3>

<button onClick={() => dispatch(increment())}>Increment</button>

<button onClick={() => dispatch(decrement())}>Decrement</button>

<button onClick={() => dispatch(reset())}>Reset</button>

</div>

);

};

export default Counter;

Now, import and use this component in App.jsx:

import Counter from './components/Counter';

function App() {

return (

<div className="App">

<h1>Redux Counter App</h1>

<Counter />

</div>

);

}

export default App;

Run your app with npm run dev. You should now see a counter that increments, decrements, and resetsall managed by Redux.

Step 8: Handle Async Actions with createAsyncThunk

Real-world applications often need to fetch data from APIs. Redux Toolkit provides createAsyncThunk to handle asynchronous logic cleanly.

Lets create a simple user data fetcher. In src/store/userSlice.js:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

import axios from 'axios';

// Async thunk to fetch user data

export const fetchUser = createAsyncThunk(

'user/fetchUser',

async (_, { rejectWithValue }) => {

try {

const response = await axios.get('https://jsonplaceholder.typicode.com/users/1');

return response.data;

} catch (error) {

return rejectWithValue(error.response?.data || 'Failed to fetch user');

}

}

);

const userSlice = createSlice({

name: 'user',

initialState: {

data: null,

loading: false,

error: null,

},

reducers: {},

extraReducers: (builder) => {

builder

.addCase(fetchUser.pending, (state) => {

state.loading = true;

state.error = null;

})

.addCase(fetchUser.fulfilled, (state, action) => {

state.loading = false;

state.data = action.payload;

})

.addCase(fetchUser.rejected, (state, action) => {

state.loading = false;

state.error = action.payload;

});

},

});

export default userSlice.reducer;

Update your store to include the new reducer:

import { configureStore } from '@reduxjs/toolkit';

import counterReducer from './counterSlice';

import userReducer from './userSlice';

const store = configureStore({

reducer: {

counter: counterReducer,

user: userReducer,

},

});

export default store;

Create a new component to display the user data: src/components/User.jsx:

import React from 'react';

import { useSelector, useDispatch } from 'react-redux';

import { fetchUser } from '../store/userSlice';

const User = () => {

const { data, loading, error } = useSelector(state => state.user);

const dispatch = useDispatch();

const handleFetch = () => {

dispatch(fetchUser());

};

return (

<div>

<h3>User Data</h3>

<button onClick={handleFetch}>Load User</button>

{loading && <p>Loading...</p>}

{error && <p style={{ color: 'red' }}>Error: {error}</p>}

{data && (

<div>

<p>Name: {data.name}</p>

<p>Email: {data.email}</p>

<p>Phone: {data.phone}</p>

</div>

)}

</div>

);

};

export default User;

Add it to your App.jsx alongside the counter:

import Counter from './components/Counter';

import User from './components/User';

function App() {

return (

<div className="App">

<h1>Redux Demo App</h1>

<Counter />

<User />

</div>

);

}

export default App;

Now your app can handle both synchronous and asynchronous state changes using Redux patterns.

Step 9: Persist State Across Sessions (Optional)

By default, Redux state resets when the page reloads. To persist state (e.g., user preferences, login tokens), use redux-persist:

npm install redux-persist

Update src/store/store.js:

import { configureStore } from '@reduxjs/toolkit';

import { persistStore, persistReducer } from 'redux-persist';

import storage from 'redux-persist/lib/storage';

import counterReducer from './counterSlice';

import userReducer from './userSlice';

const persistConfig = {

key: 'root',

storage,

whitelist: ['counter'], // only persist counter

};

const persistedCounterReducer = persistReducer(persistConfig, counterReducer);

const store = configureStore({

reducer: {

counter: persistedCounterReducer,

user: userReducer,

},

});

const persistor = persistStore(store);

export { store, persistor };

Update src/main.jsx to wrap the app with PersistGate:

import React from 'react'

import ReactDOM from 'react-dom/client'

import App from './App.jsx'

import './index.css'

import { store, persistor } from './store/store'

import { Provider } from 'react-redux'

import { PersistGate } from 'redux-persist/integration/react'

ReactDOM.createRoot(document.getElementById('root')).render(

<React.StrictMode>

<Provider store={store}>

<PersistGate loading={null} persistor={persistor}>

<App />

</PersistGate>

</Provider>

</React.StrictMode>

)

Now, when you refresh the page, the counter value persists.

Best Practices

Organize State by Feature, Not Type

A common mistake is grouping reducers by type: userReducer.js, productReducer.js, cartReducer.js. Instead, organize by feature or domain: features/user, features/cart. This improves maintainability as your app scales.

Structure your project like this:

src/

??? features/

? ??? user/

? ? ??? userSlice.js

? ? ??? User.jsx

? ??? cart/

? ? ??? cartSlice.js

? ? ??? Cart.jsx

? ??? products/

? ??? productSlice.js

? ??? ProductList.jsx

??? store/

? ??? store.js

? ??? persistConfig.js

??? App.jsx

This structure makes it easy to locate all code related to a feature and enables easier code splitting or lazy loading later.

Use TypeScript for Type Safety

If youre using TypeScript, define types for your state and actions. This prevents runtime errors and improves developer experience.

Example with TypeScript in counterSlice.ts:

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CounterState {

value: number;

}

const initialState: CounterState = {

value: 0,

};

const counterSlice = createSlice({

name: 'counter',

initialState,

reducers: {

increment: (state) => {

state.value += 1;

},

decrement: (state) => {

state.value -= 1;

},

setCount: (state, action: PayloadAction) => {

state.value = action.payload;

},

},

});

export const { increment, decrement, setCount } = counterSlice.actions;

export default counterSlice.reducer;

TypeScript will now catch errors like passing a string to setCount instead of a number.

Avoid Deeply Nested State

Redux state should be flat and normalized. Avoid nesting objects or arrays too deeply. Use lookup tables for relationships.

Bad:

{

user: {

name: 'John',

posts: [

{

id: 1,

title: 'Post 1',

comments: [

{ id: 101, text: 'Nice!', author: 'Jane' },

{ id: 102, text: 'Great!', author: 'Bob' }

]

}

]

}

}

Good:

{

users: {

1: { id: 1, name: 'John' }

},

posts: {

1: { id: 1, title: 'Post 1', authorId: 1 }

},

comments: {

101: { id: 101, text: 'Nice!', postId: 1, authorId: 2 },

102: { id: 102, text: 'Great!', postId: 1, authorId: 3 }

}

}

Normalization makes updates more efficient and avoids redundant data.

Use Selectors for Derived State

Instead of accessing state directly in components, create selectors. This improves performance and encapsulates logic.

Use createSelector from Redux Toolkit:

import { createSelector } from '@reduxjs/toolkit';

const selectCounter = (state) => state.counter;

export const selectCounterValue = createSelector(

[selectCounter],

(counter) => counter.value

);

export const selectIsEven = createSelector(

[selectCounterValue],

(value) => value % 2 === 0

);

Then use in your component:

const count = useSelector(selectCounterValue);

const isEven = useSelector(selectIsEven);

Selectors are memoized, so they only recompute when their inputs changegreat for performance.

Dont Put Everything in Redux

Redux is powerful, but not always necessary. Use it for global, shared, or persistent state. Local UI state (like form inputs, modals, or loading spinners) is better handled with Reacts useState or useReducer.

Ask yourself: Will this state be needed by multiple components across different parts of the app? If not, keep it local.

Write Meaningful Action Types

Even though Redux Toolkit auto-generates action types, ensure theyre descriptive. Avoid generic names like UPDATE or SET. Use user/LOGIN_SUCCESS or cart/ADD_ITEM.

This improves debugging and logging. Tools like Redux DevTools rely on clear action names to help you trace state changes.

Use Middleware for Side Effects

Redux Thunk (included by default in Redux Toolkit) is sufficient for most async logic. For more complex workflows (e.g., sagas, side effects with cancellation), consider @reduxjs/toolkit/queryRedux Toolkits built-in data fetching and caching layer.

Example with RTK Query:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const api = createApi({

baseQuery: fetchBaseQuery({ baseUrl: '/api' }),

endpoints: (builder) => ({

getUser: builder.query<User, number>({

query: (id) => /users/${id},

}),

}),

});

export const { useGetUserQuery } = api;

RTK Query eliminates the need for manual action creators, reducers, and loading/error statesit handles all of it automatically.

Tools and Resources

Redux DevTools Extension

Install the official Redux DevTools browser extension for Chrome or Firefox. It allows you to:

  • See every action dispatched
  • Track state changes over time
  • Time-travel debug by reverting to previous states
  • Export/import state snapshots

Redux Toolkit automatically enables DevTools integration, so no extra configuration is needed.

Redux Toolkit

Redux Toolkit is the official, recommended way to write Redux logic. It includes:

  • createSlice reduces boilerplate for reducers and actions
  • createAsyncThunk simplifies async logic
  • configureStore auto-configures middleware and DevTools
  • createSelector memoized selectors
  • createApi RTK Query for data fetching

Always use Redux Toolkit over vanilla Redux unless you have a very specific reason not to.

Redux Persist

For persisting state across page reloads, redux-persist is the standard solution. It supports localStorage, sessionStorage, and even AsyncStorage for React Native.

React Developer Tools

Pair Redux DevTools with the React DevTools extension to inspect component props and state. You can see exactly which components re-render when state changes.

ESLint Plugins

Use eslint-plugin-redux-saga or eslint-plugin-redux to catch common mistakes like mutating state or incorrect action types.

Documentation and Learning Resources

Code Sandbox Templates

Use pre-configured templates to jumpstart your project:

Real Examples

Example 1: E-Commerce Shopping Cart

Consider an online store with these features:

  • Add/remove products from cart
  • Update quantity
  • Apply discount codes
  • Calculate total price
  • Persist cart on reload

Using Redux Toolkit, youd create a cartSlice.js:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

import { products } from '../data/products'; // mock data

export const addToCart = createAsyncThunk(

'cart/addToCart',

async (productId, { getState }) => {

const product = products.find(p => p.id === productId);

return product;

}

);

const cartSlice = createSlice({

name: 'cart',

initialState: {

items: [],

total: 0,

discount: 0,

},

reducers: {

removeFromCart: (state, action) => {

state.items = state.items.filter(item => item.id !== action.payload);

},

updateQuantity: (state, action) => {

const item = state.items.find(i => i.id === action.payload.id);

if (item) item.quantity = action.payload.quantity;

},

applyDiscount: (state, action) => {

state.discount = action.payload;

},

},

extraReducers: (builder) => {

builder.addCase(addToCart.fulfilled, (state, action) => {

const existingItem = state.items.find(item => item.id === action.payload.id);

if (existingItem) {

existingItem.quantity += 1;

} else {

state.items.push({ ...action.payload, quantity: 1 });

}

state.total = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0) - state.discount;

});

},

});

export const { removeFromCart, updateQuantity, applyDiscount } = cartSlice.actions;

export default cartSlice.reducer;

Use useSelector to compute total items:

const totalItems = useSelector(state => state.cart.items.reduce((sum, item) => sum + item.quantity, 0));

With redux-persist, the cart persists between sessions, enhancing user experience.

Example 2: Real-Time Dashboard with WebSocket

A stock trading dashboard needs to update prices in real time. Use createAsyncThunk to handle WebSocket connections:

export const connectToWebSocket = createAsyncThunk(

'dashboard/connect',

async (_, { dispatch }) => {

const socket = new WebSocket('wss://api.stockstream.com');

socket.onmessage = (event) => {

const data = JSON.parse(event.data);

dispatch(updatePrice(data));

};

return socket;

}

);

const dashboardSlice = createSlice({

name: 'dashboard',

initialState: {

prices: {},

socket: null,

},

reducers: {

updatePrice: (state, action) => {

state.prices[action.payload.symbol] = action.payload.price;

},

},

});

Components can then subscribe to price changes and update charts instantly.

Example 3: Multi-User Collaboration App

In a collaborative document editor, multiple users edit simultaneously. Redux ensures all clients receive the same state updates via a shared store. Actions like text/INSERT or text/DELETE are dispatched and synchronized across clients using WebSockets or a backend service.

Each change becomes an immutable action, enabling conflict resolution and undo/redo functionality through state history.

FAQs

Is Redux still relevant in 2024?

Yes. While Reacts Context API and Zustand have gained popularity for simpler state needs, Redux remains the gold standard for large-scale applications requiring predictable state management, middleware support, and developer tooling. Redux Toolkit has modernized the ecosystem, making it more accessible than ever.

When should I not use Redux?

Use Redux only when you have complex state logic that crosses multiple components, requires middleware (like logging or async handling), or needs to persist across sessions. For simple apps with local state, Reacts built-in hooks are sufficient and less complex.

Does Redux slow down my app?

No. Redux is highly performant when used correctly. The overhead of dispatching actions is negligible compared to the benefits of predictable state. Use selectors to avoid unnecessary re-renders, and leverage React.memo or React.memo for expensive components.

Can I use Redux without React?

Absolutely. Redux is framework-agnostic. You can use it with Vue, Angular, Svelte, or vanilla JavaScript. The store, actions, and reducers remain the same; only the binding layer changes.

Whats the difference between Redux and Zustand?

Zustand is a lightweight alternative with less boilerplate. Its ideal for small to medium apps. Redux Toolkit offers more structure, middleware, DevTools, and scalability for enterprise applications. Choose Zustand for simplicity; choose Redux for control and ecosystem support.

How do I test Redux logic?

Test reducers as pure functions:

import counterReducer from './counterSlice';

test('should increment counter', () => {

const initialState = { value: 0 };

const action = { type: 'counter/increment' };

const newState = counterReducer(initialState, action);

expect(newState.value).toBe(1);

});

Test async thunks with mocked APIs using libraries like jest and axios-mock-adapter.

How do I handle authentication with Redux?

Create an authSlice that manages:

  • user object
  • token
  • loading state
  • error state

Use createAsyncThunk for login/logout requests. Store the token in localStorage or HTTP-only cookies. On app load, check for a token and dispatch fetchUser to hydrate the state.

Conclusion

Implementing Redux is not just about adding a libraryits about adopting a disciplined approach to state management that scales with your application. With Redux Toolkit, the once-daunting setup process has been streamlined into a few intuitive functions. By following the patterns outlined in this guideorganizing state by feature, using selectors, handling async logic cleanly, and persisting critical datayoull build applications that are easier to debug, test, and maintain.

Redux is not a silver bullet. It adds structure, and with structure comes responsibility. Use it where it adds valuenot everywhere. But when your app grows beyond simple component-level state, Redux becomes indispensable. The investment in learning it pays off in long-term code quality, team collaboration, and system reliability.

Now that you understand how to implement Redux from the ground up, experiment with real projects. Start small: add a counter, then a user profile, then a shopping cart. Gradually incorporate async actions, persistence, and TypeScript. Over time, youll develop an instinct for when and how to use Redux effectivelyand youll wonder how you ever built complex apps without it.