Redux Toolkit

Modern Redux state management made simple

🔧 What is Redux Toolkit?

Redux Toolkit is the official, recommended way to write Redux logic. It simplifies Redux setup with less boilerplate code, built-in best practices, and powerful tools like createSlice and configureStore for efficient state management.


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

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: state => { state.value += 1; }
  }
});
                                    

Result:

Redux Toolkit reduces boilerplate and makes Redux easier to use.

Core Redux Toolkit APIs

🏪

configureStore

Creates Redux store with defaults

const store = configureStore({
  reducer: { counter: counterReducer }
});
🍕

createSlice

Generates actions and reducers

const slice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: { addTodo }
});

createAsyncThunk

Handles async logic easily

const fetchUser = createAsyncThunk(
  'users/fetch',
  async (id) => await api.get(id)
);
🎣

Hooks

Access state and dispatch

const count = useSelector(s => s.count);
const dispatch = useDispatch();

🔹 Setting Up Redux Toolkit

Start by installing Redux Toolkit and React-Redux, then create a store with configureStore. This setup includes Redux DevTools and middleware automatically.

# Install Redux Toolkit and React-Redux
npm install @reduxjs/toolkit react-redux
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

const store = configureStore({
  reducer: {
    counter: counterReducer
  }
});

export default store;
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

Output:

Redux store is now available to all components in your app.

🔹 Creating a Slice

A slice contains reducer logic and actions for a single feature. createSlice automatically generates action creators and action types, eliminating boilerplate code.

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: (state) => {
      // Redux Toolkit uses Immer, so you can "mutate" state
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
    reset: (state) => {
      state.value = 0;
    }
  }
});

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

// Export reducer
export default counterSlice.reducer;

Output:

Actions and reducer are automatically generated from the slice.

🔹 Using Redux in Components

Use useSelector to read state and useDispatch to dispatch actions. These hooks connect your React components to the Redux store seamlessly.

// Counter.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, incrementByAmount, reset } from './counterSlice';

function Counter() {
  // Select state from store
  const count = useSelector((state) => state.counter.value);
  
  // Get dispatch function
  const dispatch = useDispatch();

  return (
    <div>
      <h2>Count: {count}</h2>
      
      <button onClick={() => dispatch(increment())}>
        Increment
      </button>
      
      <button onClick={() => dispatch(decrement())}>
        Decrement
      </button>
      
      <button onClick={() => dispatch(incrementByAmount(5))}>
        Add 5
      </button>
      
      <button onClick={() => dispatch(reset())}>
        Reset
      </button>
    </div>
  );
}

export default Counter;

Output:

Counter displays current value and buttons update the Redux state.

🔹 Async Operations with createAsyncThunk

createAsyncThunk handles async logic like API calls. It automatically dispatches pending, fulfilled, and rejected actions based on the promise lifecycle.

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

// Async thunk for fetching user
export const fetchUser = createAsyncThunk(
  'user/fetchUser',
  async (userId) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  }
);

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.error.message;
      });
  }
});

export default userSlice.reducer;
// UserProfile.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUser } from './userSlice';

function UserProfile({ userId }) {
  const dispatch = useDispatch();
  const { data, loading, error } = useSelector((state) => state.user);

  useEffect(() => {
    dispatch(fetchUser(userId));
  }, [dispatch, userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!data) return null;

  return (
    <div>
      <h2>{data.name}</h2>
      <p>{data.email}</p>
    </div>
  );
}

Output:

Shows loading state, then displays user data or error message.

🔹 Multiple Slices Example

Real apps have multiple slices for different features. Combine them in configureStore to create a complete state management solution.

// todosSlice.js
import { createSlice } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      state.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      });
    },
    toggleTodo: (state, action) => {
      const todo = state.find(t => t.id === action.payload);
      if (todo) todo.completed = !todo.completed;
    },
    deleteTodo: (state, action) => {
      return state.filter(t => t.id !== action.payload);
    }
  }
});

export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;
// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import todosReducer from './todosSlice';
import userReducer from './userSlice';

const store = configureStore({
  reducer: {
    counter: counterReducer,
    todos: todosReducer,
    user: userReducer
  }
});

export default store;

Output:

Store now manages counter, todos, and user state independently.

🔹 Redux Toolkit Best Practices

Follow these guidelines to write clean, maintainable Redux code with Redux Toolkit:

✅ Do:

  • Use createSlice for all state logic
  • Use createAsyncThunk for async operations
  • Keep slices focused on single features
  • Use TypeScript for better type safety
  • Organize slices by feature, not by type

❌ Don't:

  • Write action types and creators manually
  • Use plain Redux without Redux Toolkit
  • Put all state in one giant slice
  • Mutate state outside of createSlice reducers
  • Forget to handle loading and error states
// ✅ Good: Focused slice with clear actions
const userSlice = createSlice({
  name: 'user',
  initialState: { profile: null, loading: false },
  reducers: {
    setProfile: (state, action) => {
      state.profile = action.payload;
    }
  }
});

// ❌ Bad: Manual action types (unnecessary with RTK)
const SET_PROFILE = 'user/setProfile';
const setProfile = (payload) => ({ type: SET_PROFILE, payload });

🧠 Test Your Knowledge

Which Redux Toolkit function creates actions and reducers together?