Introduction
Redux keeps all your app state in one store and updates it through a strict action → reducer → new state flow. That makes state changes predictable and easy to debug with time-travel tools.
It shines when:
- Many components at different tree levels read the same data.
- You need an audit trail of every state change.
- Complex async workflows (API calls, optimistic updates) require clear orchestration.
The core principle of Redux is a single source of truth—all state is stored in a centralized store. Changes follow a strict flow:
- Dispatch an Action – Defines what should happen.
- Reducer Updates State – A pure function modifies state without side effects.
- New State is Available – Components re-render with the updated state.
Redux is often used with React via react-redux, but it can work with any framework or even standalone.
Getting Started with Redux
Installing Redux and React-Redux
-
redux: Core library. -
react-redux: Integration tools for React applications.
Creating a Redux Store
The store is the centralized location where application state is maintained.
import { createStore } from 'redux';
import { createStore } from 'redux';
// Initial state
const initialState = {
counter: 0,
};
// Reducer function
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, counter: state.counter + 1 };
case 'DECREMENT':
return { ...state, counter: state.counter - 1 };
default:
return state;
}
}
// Creating store
const store = createStore(counterReducer);
console.log(store.getState()); // { counter: 0 }Actions: Defining Intentions
Actions describe events in the application and have a required type property.
const incrementAction = { type: 'INCREMENT' };
const decrementAction = { type: 'DECREMENT' };Connecting Redux with React
Use Provider from react-redux to make the store available to all components.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import counterReducer from './reducers';
import { createStore } from 'redux';
import App from './App';
import counterReducer from './reducers';
import { createStore } from 'redux';
const store = createStore(counterReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);Using Redux in Components
Use useSelector to access the store and useDispatch to send actions.
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
function Counter() {
const counter = useSelector(state => state.counter);
const dispatch = useDispatch();
return (
<div>
<h1>Counter: {counter}</h1>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</div>
);
}
export default Counter;Simplifying Redux with Redux Toolkit
Redux Toolkit (RTK) reduces boilerplate code and improves efficiency.
Installing Redux Toolkit
Using Redux Toolkit
RTK provides createSlice to define reducers and actions in one place.
import { configureStore, createSlice } from '@reduxjs/toolkit';
import { configureStore, createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
},
});
export default store;Middleware in Redux
Middleware enhances Redux functionality by handling side effects like logging and async operations.
Example: Logging Middleware
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
const store = createStore(rootReducer, applyMiddleware(logger));Handling Asynchronous Operations with Redux Thunk
Redux is synchronous by default. To handle API calls, use redux-thunk.
Installing Redux Thunk
Async Action Using Thunk
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
const store = createStore(rootReducer, applyMiddleware(thunk));
export const fetchUsers = () => async dispatch => {
dispatch({ type: 'FETCH_USERS_REQUEST' });
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await response.json();
dispatch({ type: 'FETCH_USERS_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_USERS_FAILURE', payload: error.message });
}
};Real-World Example: E-Commerce Store
Consider a store selling pet products. The app needs to handle:
- Product listings: Fetching and displaying products from an API.
- Shopping cart management: Adding/removing products and updating totals.
- User authentication: Managing user sessions for order processing.
- Order processing: Handling payment and shipping details.
Each of these is managed as a separate Redux slice for modularity and scalability.
Product Slice
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchProducts = createAsyncThunk(
'products/fetchProducts',
async () => {
const response = await fetch('https://api.example.com/cat-products');
return response.json();
}
);
const productsSlice = createSlice({
name: 'products',
initialState: { items: [], status: 'idle', error: null },
extraReducers: builder => {
builder
.addCase(fetchProducts.pending, state => {
state.status = 'loading';
})
.addCase(fetchProducts.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
.addCase(fetchProducts.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
});
},
});
export default productsSlice.reducer;This modular approach makes the application scalable and easy to maintain.
Conclusion
Don’t reach for Redux by default — Context or Zustand cover most small-to-medium apps. Pick Redux when you need middleware pipelines, DevTools time-travel, or a strict event-log architecture. If you do choose it, use Redux Toolkit from the start — vanilla Redux boilerplate isn’t worth writing anymore.