Redux Toolkit

Modern Redux state management for Next.js

🗄️ What is Redux Toolkit?

Redux Toolkit is the official, recommended way to write Redux logic. It simplifies store setup, reduces boilerplate code, and includes powerful tools like createSlice and createAsyncThunk for managing global application state efficiently.


// Simple Redux Toolkit slice
import { createSlice } from '@reduxjs/toolkit'

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

export const { increment, decrement } = counterSlice.actions
                                    

Key Redux Toolkit Features

🏪

Store Setup

Easy store configuration

configureStore({
  reducer: {
    counter: counterReducer
  }
})
✂️

Slices

Combine reducers and actions

createSlice({
  name: 'user',
  initialState,
  reducers: {}
})

Async Thunks

Handle async operations

createAsyncThunk(
  'users/fetch',
  async () => fetchUsers()
)
🔍

Selectors

Access state efficiently

useSelector(
  state => state.counter.value
)

🔹 Setup Redux Toolkit Store

Create a Redux store using configureStore and wrap your Next.js app with the Provider. This makes the store available to all components in your application.

// lib/store.js
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counterSlice'

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

// app/providers.jsx
'use client'
import { Provider } from 'react-redux'
import { store } from '@/lib/store'

export default function Providers({ children }) {
  return <Provider store={store}>{children}</Provider>
}

// app/layout.jsx
import Providers from './providers'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

🔹 Create a Slice

Slices combine state, reducers, and actions in one place. Use createSlice to define your state logic with automatic action creators and immutable updates using Immer.

// lib/features/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
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

State Structure:

{ counter: { value: 0 } }

🔹 Using Redux in Components

Access state with useSelector and dispatch actions with useDispatch. These hooks connect your components to the Redux store for reading and updating state.

// components/Counter.jsx
'use client'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement, incrementByAmount } from '@/lib/features/counterSlice'

export default function Counter() {
  const count = useSelector((state) => state.counter.value)
  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>
    </div>
  )
}

Output:

Count: 0

🔹 Async Operations with createAsyncThunk

Handle API calls and async logic with createAsyncThunk. It automatically dispatches pending, fulfilled, and rejected actions based on your async function's lifecycle.

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

export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async () => {
    const response = await fetch('/api/users')
    return response.json()
  }
)

const userSlice = createSlice({
  name: 'users',
  initialState: {
    list: [],
    status: 'idle',
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading'
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.list = action.payload
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})

export default userSlice.reducer

🔹 Using Async Thunks in Components

Dispatch async thunks like regular actions. The component can track loading states and handle errors automatically through the Redux state.

// components/UserList.jsx
'use client'
import { useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { fetchUsers } from '@/lib/features/userSlice'

export default function UserList() {
  const dispatch = useDispatch()
  const { list, status, error } = useSelector((state) => state.users)

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUsers())
    }
  }, [status, dispatch])

  if (status === 'loading') return <div>Loading...</div>
  if (status === 'failed') return <div>Error: {error}</div>

  return (
    <ul>
      {list.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

🧠 Test Your Knowledge

What function creates a Redux slice?