Complete React Redux Tutorial (Beginner to Advanced)
Table of Contents
- Introduction
- Core Redux Concepts
- Store Setup
- Actions and Reducers
- Store Configuration
- Async Logic (Thunks & createAsyncThunk)
- Selectors & Memoization
- Normalization with Entity Adapter
- RTK Query
- Using Redux with React
- Best Practices
- Common Mistakes
- Redux vs Redux Toolkit
- TypeScript with Redux
- Real-World App Structure
- Conclusion
Introduction
Redux is a predictable state container for JavaScript apps, most commonly used with React. It helps you manage your application's state in a centralized way, enabling better debugging, testing, and consistency.
This tutorial will take you from beginner to advanced level and cover:
- Core Redux concepts (store, actions, reducers, dispatch)
- Classic Redux vs Redux Toolkit
- JavaScript and TypeScript examples
- RTK Query for API calls
- Real-world usage, best practices, and common pitfalls
Core Redux Concepts
Redux operates on a few fundamental concepts:
- Store: The global state container.
- Actions: Plain objects that describe what happened.
- Reducers: Pure functions that update the state based on actions.
- Dispatch: The method used to send actions to the store.
- Selectors: Functions that retrieve specific parts of the state.
Imagine a restaurant:
- The customer (React component) fills out an order form (action).
- The waiter (dispatch function) delivers the order.
- The kitchen (reducer) prepares the meal based on the order.
- The counter (store) holds the completed order (state).
Store Setup
Classic Redux
// store.js
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
Redux Toolkit
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
Actions and Reducers
Classic Redux
// actions.js
export const increment = () => ({ type: 'INCREMENT' });
// reducer.js
const initialState = { count: 0 };
export default function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
}
Redux Toolkit
// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { count: 0 },
reducers: {
increment(state) {
state.count += 1;
},
},
});
export const { increment } = counterSlice.actions;
export default counterSlice.reducer;
Store Configuration
Classic Redux requires manual setup of middleware like redux-thunk
. Redux Toolkit includes thunk by default and sets up DevTools automatically.
Async Logic (Thunks & createAsyncThunk)
Classic Redux Thunk
// thunk.js
export const fetchUser = (id) => async (dispatch) => {
dispatch({ type: 'FETCH_USER_START' });
try {
const res = await fetch(`/api/user/${id}`);
const data = await res.json();
dispatch({ type: 'FETCH_USER_SUCCESS', payload: data });
} catch (err) {
dispatch({ type: 'FETCH_USER_ERROR', error: err });
}
};
RTK createAsyncThunk
// userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchUser = createAsyncThunk(
'user/fetchUser',
async (id) => {
const res = await fetch(`/api/user/${id}`);
return await res.json();
}
);
const userSlice = createSlice({
name: 'user',
initialState: { data: null, status: 'idle' },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.data = action.payload;
state.status = 'succeeded';
})
.addCase(fetchUser.rejected, (state) => {
state.status = 'failed';
});
},
});
Selectors & Memoization
// selectors.js
export const selectCount = (state) => state.counter.count;
// Reselect example
import { createSelector } from 'reselect';
export const selectEvenCount = createSelector(
[selectCount],
(count) => count % 2 === 0
);
Normalization with Entity Adapter
// postsSlice.js
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
const postsAdapter = createEntityAdapter();
const initialState = postsAdapter.getInitialState();
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
addPost: postsAdapter.addOne,
removePost: postsAdapter.removeOne,
},
});
export const {
selectAll: selectAllPosts,
} = postsAdapter.getSelectors((state) => state.posts);
RTK Query
// apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => `user/${id}`,
}),
}),
});
export const { useGetUserQuery } = api;
Using Redux with React
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';
ReactDOM.render(
,
document.getElementById('root')
);
Best Practices
- Use Redux Toolkit for new projects.
- Keep state normalized and avoid deeply nested structures.
- Don’t mutate state directly outside of Immer.
- Split logic by feature using slices.
- Use memoized selectors with
createSelector
. - Use local component state for transient UI values.
Common Mistakes
- Mutating state directly in reducers.
- Dispatching actions from reducers (not allowed).
- Storing non-serializable values in state.
- Forgetting to wrap the app with
Provider
.
Redux vs Redux Toolkit
Feature | Classic Redux | Redux Toolkit |
---|---|---|
Boilerplate | High | Low |
Thunk Middleware | Manual | Built-in |
Immutability | Manual | Via Immer |
DevTools | Manual Setup | Automatic |
Code Split | Manual | Slices |
TypeScript with Redux
// store.ts
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
Real-World App Structure
/features
– Slice-based feature folders/app/store.js
– Store configuration/api
– RTK Query endpoints/components
– Reusable UI components
Conclusion
Redux is a powerful tool for managing global state. Redux Toolkit makes it easier and less error-prone. Use slices, selectors, thunks, and RTK Query to manage your data cleanly and efficiently.