How to Add Redux to Your Next.js App

Redux is a global state management library. It's used to centralize all your application's state inside one store. Which means all your states are in one place, making it easier to maintain. It defines core concepts on how to update your state that makes it more predictable. Leading to less bugs. It is compatible with many JavaScript frameworks but most commonly used with React. In this blog we will be learning how to add Redux to your Next.js App.

Jordan Wu profile picture

Jordan Wu

5 min·Posted 

Road in City during Sunset Image.
Road in City during Sunset Image.
Table of Contents

What is Redux?

Redux is based on three principles that will make state management more maintainable and predictable. States are data used in web applications that could be what you see on the web page to how you interact with the application. As web applications grow in complexity so does the state it needs to manage. What became increasingly complicated was updating many states at the same time. This was difficult to do until Redux. It made state management a breeze and improved developer experience.

Three Principles

Single Source of Truth

The global state of your application is stored in an object tree within a single store. You can think of the store as one JavaScript object that represents your application state. This makes the state predictable and easier to manage.

{
  playlist: {
    autoplay: false,
    repeat: 0,
    shuffle: false,
    trackIndex: 0,
    tracks: [
      {
        title: 'Nothing But the Radio',
        src: 'https://tracks.jordanwu.xyz/black-bear-the-sex-mixtape/Nothin-But-the-Radio.mp3',
        artist: 'Blackbear',
        album: 'Sex the Mixtape',
        thumbnail: 'https://images.jordanwu.xyz/albums/black-bear-the-sex-mixtape/black-bear-sex-the-mixtape.jpeg',
        thumbnailSmall:
          'https://images.jordanwu.xyz/albums/black-bear-the-sex-mixtape/black-bear-sex-the-mixtape-96x96.jpeg',
        artworks: [
          {
            src: 'https://images.jordanwu.xyz/albums/black-bear-the-sex-mixtape/black-bear-sex-the-mixtape-96x96.jpeg',
            sizes: '96x96',
            type: 'image/jpeg',
          },
          {
            src: 'https://images.jordanwu.xyz/albums/black-bear-the-sex-mixtape/black-bear-sex-the-mixtape-128x128.jpeg',
            sizes: '128x128',
            type: 'image/jpeg',
          },
          {
            src: 'https://images.jordanwu.xyz/albums/black-bear-the-sex-mixtape/black-bear-sex-the-mixtape-192x192.jpeg',
            sizes: '192x192',
            type: 'image/jpeg',
          },
          {
            src: 'https://images.jordanwu.xyz/albums/black-bear-the-sex-mixtape/black-bear-sex-the-mixtape-192x192.jpeg',
            sizes: '256x256',
            type: 'image/jpeg',
          },
          {
            src: 'https://images.jordanwu.xyz/albums/black-bear-the-sex-mixtape/black-bear-sex-the-mixtape-384x384.jpeg',
            sizes: '384x384',
            type: 'image/jpeg',
          },
          {
            src: 'https://images.jordanwu.xyz/albums/black-bear-the-sex-mixtape/black-bear-sex-the-mixtape-512x512.jpeg',
            sizes: '512x512',
            type: 'image/jpeg',
          },
        ],
      },
      ...
    ],
  }
}

In this example our store has a playlist that contains the state of our audio player. It has a list of tracks and keeps track of which track is selected as trackIndex. Other states are used to determine what the shuffle and repeat icon should be displayed.

State is read-only

The only way to change the state is to emit an action, an object describing what happened. An action is a plain object that represents an intention to change the state. For the state to be read-only the state is an immutable object and cannot be mutated. This ensures that the state is not modified directly, making the state changes traceable and predictable.

const setAutoplay = {
  type: 'SET_AUTOPLAY',
  payload: true,
}

const setNextPlaylistTrack = {
  type: 'SET_NEXT_PLAYLIST_TRACK',
}

const setPreviousPlaylistTrack = {
  type: 'SET_PREVIOUS_PLAYLIST_TRACK',
}

const setPlaylistTrack = {
  type: 'SET_PLAYLIST_TRACK',
  payload: 1,
}

const setRepeat = {
  type: 'SET_REPEAT',
  payload: 1,
}

const toggleShuffle = {
  type: 'TOGGLE_SHUFFLE',
}

In the example we have many actions that will be used to update our playlist state in the store. Each action consists of two fields. type is the name of the action and payload is optional data that is needed to update playlist state.

Changes are made with pure functions

To specify how the state tree is transformed by actions, you write pure reducers. Reducers are just pure functions that take the previous state and an action, and return the next state. Pure functions are functions that return the exact same output for given inputs. Remember to return new state objects, instead of mutating the previous state.

function playlistReducer(state, action) {
  switch (action.type) {
    case 'SET_AUTOPLAY':
      return {
        ...state,
        autoplay: !state.autoplay,
      }
    case 'SET_NEXT_PLAYLIST_TRACK':
      return {
        ...state,
        trackIndex: state.trackIndex + 1,
      }
    case 'SET_PREVIOUS_PLAYLIST_TRACK':
      return {
        ...state,
        trackIndex: state.trackIndex - 1,
      }
    case 'SET_PLAYLIST_TRACK':
      return {
        ...state,
        trackIndex: action.payload,
      }
    case 'SET_REPEAT':
      return {
        ...state,
        repeat: action.payload,
      }
    case 'TOGGLE_SHUFFLE':
      return {
        ...state,
        shuffle: !state.shuffle,
      }
    default:
      return state
  }
}

In the example is playlistReducer that is a pure function that will handle updating our state using spread syntax that returns a new object of the state. Not returning a new object will result in the store not knowing a state has been updated as it compares objects as references, an address in memory.

How to Add Redux to Next.js App Router

Next.js is a full stack framework and supports server side rendering which presents some unique challenges when using Redux. This approach focuses on the App Router architecture of Next.js and will be adding a playlist state. Start by downloading @reduxjs/toolkit and react-redux dependencies.

pnpmyarnnpm
pnpm add @reduxjs/toolkit react-redux

Redux Toolkit RTK is the current standard to write Redux logic. It aligns with Redux Style Guide: Best Practices, simplifies many Redux tasks, prevents common mistakes, and makes it easier to maintain your Redux logic. Redux state is typically organized into "slices". Add a file called createAppSlice.ts in the lib directory.

File Imagesrc/lib/createAppSlice.ts
import { asyncThunkCreator, buildCreateSlice } from '@reduxjs/toolkit'

// `buildCreateSlice` allows us to create a slice with async thunks.
export const createAppSlice = buildCreateSlice({
  creators: { asyncThunk: asyncThunkCreator },
})

This will be our application slice used to create our playlistSlice.ts.

File Imagesrc/lib/features/playlist/playlistSlice.ts
import { createAppSlice } from '@/lib/createAppSlice'
import { TRACKS } from '@/utils/tracks'

import type { Track } from '@/utils/tracks'
import type { PayloadAction } from '@reduxjs/toolkit'

export interface PlaylistSliceState {
  autoplay: boolean
  repeat: number
  shuffle: boolean
  trackIndex: number
  tracks: Track[]
}

const initialState: PlaylistSliceState = {
  autoplay: false,
  repeat: 0,
  shuffle: false,
  trackIndex: 0,
  tracks: TRACKS,
}

export const playlistSlice = createAppSlice({
  name: 'playlist',
  initialState,
  reducers: (create) => ({
    setAutoplay: create.reducer((state, action: PayloadAction<boolean>) => {
      state.autoplay = action.payload
    }),
    setNextPlaylistTrack: create.reducer((state) => {
      state.trackIndex += 1
    }),
    setPreviousPlaylistTrack: create.reducer((state) => {
      state.trackIndex -= 1
    }),
    setPlaylistTrack: create.reducer((state, action: PayloadAction<number>) => {
      state.trackIndex = action.payload
    }),
    setRepeat: create.reducer((state, action: PayloadAction<number>) => {
      state.repeat = action.payload
    }),
    toggleShuffle: create.reducer((state) => {
      state.shuffle = !state.shuffle
    }),
  }),
  selectors: {
    selectAutoplay: (playlist) => playlist.autoplay,
    selectRepeat: (playlist) => playlist.repeat,
    selectShuffle: (playlist) => playlist.shuffle,
    selectTrackIndex: (playlist) => playlist.trackIndex,
    selectTracks: (playlist) => playlist.tracks,
  },
})

export const {
  setAutoplay,
  setNextPlaylistTrack,
  setPlaylistTrack,
  setPreviousPlaylistTrack,
  setRepeat,
  toggleShuffle,
} = playlistSlice.actions

export const { selectAutoplay, selectRepeat, selectShuffle, selectTrackIndex, selectTracks } = playlistSlice.selectors

This file contains the initial state for the playlist state, reducers for updating the playlist state, and selectors for accessing state data. Next to create the store in store.ts.

File Imagesrc/lib/store.ts
import type { Action, ThunkAction } from '@reduxjs/toolkit'
import { combineSlices, configureStore } from '@reduxjs/toolkit'
import { playlistSlice } from './features/playlist/playlistSlice'

// `combineSlices` automatically combines the reducers using
// their `reducerPath`s, therefore we no longer need to call `combineReducers`.
const rootReducer = combineSlices(playlistSlice)
// Infer the `RootState` type from the root reducer
export type RootState = ReturnType<typeof rootReducer>

// `makeStore` encapsulates the store configuration to allow
// creating unique store instances, which is particularly important for
// server-side rendering (SSR) scenarios. In SSR, separate store instances
// are needed for each request to prevent cross-request state pollution.
export const makeStore = () => {
  return configureStore({
    reducer: rootReducer,
  })
}

// Infer the return type of `makeStore`
export type AppStore = ReturnType<typeof makeStore>
// Infer the `AppDispatch` type from the store itself
export type AppDispatch = AppStore['dispatch']
export type AppThunk<ThunkReturnType = void> = ThunkAction<ThunkReturnType, RootState, unknown, Action>

This will create our store based on our playlistSlice. Next create StoreProvider to give Next.js pages access to the store.

File Imagesrc/providers/StoreProvider.tsx
'use client'
import { makeStore } from '@/lib/store'
import { setupListeners } from '@reduxjs/toolkit/query'
import type { ReactNode } from 'react'
import { useEffect, useRef } from 'react'
import { Provider } from 'react-redux'

import type { AppStore } from '@/lib/store'

type StoreProviderProps = {
  children: ReactNode
}

export default function StoreProvider(props: StoreProviderProps) {
  const storeRef = useRef<AppStore | null>(null)

  const { children } = props

  if (!storeRef.current) {
    // Create the store instance the first time this renders
    storeRef.current = makeStore()
  }

  useEffect(() => {
    if (storeRef.current != null) {
      // configure listeners using the provided defaults
      // optional, but required for `refetchOnFocus`/`refetchOnReconnect` behaviors
      const unsubscribe = setupListeners(storeRef.current.dispatch)
      return unsubscribe
    }
  }, [])

  return <Provider store={storeRef.current}>{children}</Provider>
}

Now hook up the provider in layout.tsx.

File Imagesrc/app/layout.tsx
import StoreProvider from '@/providers/StoreProvider'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <StoreProvider>{children}</StoreProvider>
      </body>
    </html>
  )

With TypeScript it's important to have type safety which requires one additional step of adding hooks. Create a hooks.ts file.

File Imagesrc/lib/hooks.ts
// This file serves as a central hub for re-exporting pre-typed Redux hooks.
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()

That's everything you need to get Redux working in Next.js! To test this out you use the following hooks to access the Redux store.

import { useAppSelector, useAppDispatch } from '@/lib/hooks'
import {
  selectAutoplay,
  selectRepeat,
  selectShuffle,
  selectTrackIndex,
  selectTracks,
  setAutoplay,
  setNextPlaylistTrack,
  setPlaylistTrack,
  setPreviousPlaylistTrack,
  setRepeat,
  toggleShuffle,
} from '@/lib/features/playlist/playlistSlice'

export default function AudioPlayerBottomNavbar() {
  const tracks = useAppSelector(selectTracks)
  const trackIndex = useAppSelector(selectTrackIndex)
  const autoplay = useAppSelector(selectAutoplay)
  const shuffle = useAppSelector(selectShuffle)
  const repeat = useAppSelector(selectRepeat)
  const dispatch = useAppDispatch()

  dispatch(setAutoplay(false))
  dispatch(setNextPlaylistTrack())
  dispatch(setPlaylistTrack(0))
  dispatch(setPreviousPlaylistTrack())
  dispatch(setRepeat(1))
  dispatch(toggleShuffle())
}

Summary

Once you added Redux to your Next.js app. You have a centralized store that is accessible anywhere in your app. You get type checking with the help of TypeScript which makes it easier to develop and leads to increased productivity. Redux makes state management easy as your application grows in complexity. Be sure to check out Redux DevTools is a chrome extension that you can use when developing to help with debugging.

Redux DevTools of Playlist State
Redux DevTools of Playlist State

About the Author

Jordan Wu profile picture
Jordan is a full stack engineer with years of experience working at startups. He enjoys learning about software development and building something people want. What makes him happy is music. He is passionate about finding music and is an aspiring DJ. He wants to create his own music and in the process of finding is own sound.
Email icon image
Stay up to date

Get notified when I publish something new, and unsubscribe at any time.