Start with modern Redux Toolkit — why a global store exists, configureStore, a first slice with createSlice, the auto-generated actions, and wiring the Provider into a Next.js app.
Why: Redux Toolkit (RTK) is the official, modern way to use Redux — it bundles everything and removes the boilerplate the old Redux was infamous for. configureStore creates the single store; you give it a "reducers" object whose keys become the sections of your state. Note: install both packages — @reduxjs/toolkit is the toolkit, react-redux connects it to React components.
$ pnpm add @reduxjs/toolkit react-redux// app/store.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
export const store = configureStore({
// Each key here becomes a section of your global state.
// state.counter will be managed by counterReducer (built next).
reducer: {
counter: counterReducer,
},
})
// These two types describe your store, derived automatically —
// the hooks in the next lesson use them so everything stays typed.
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatchWhy: a "slice" is one section of the store bundled with the logic that changes it — the state plus the functions that update it, all in one file. createSlice asks for three things: a name, the initialState, and reducers (the list of changes that can happen). In return it generates the action creators and the reducer for you, so you never write them by hand. Note: the reducers look like they MODIFY state directly — state.value += 1 — and that is allowed here. RTK uses a library called Immer that turns those edits into a correct new copy behind the scenes.
// app/counterSlice.ts
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'
type CounterState = { value: number }
const initialState: CounterState = { value: 0 }
const counterSlice = createSlice({
name: 'counter', // a label used in action names, e.g. 'counter/increment'
initialState,
reducers: {
// Each function describes one change. The "mutating" code is safe —
// Immer turns it into a new copy for you.
increment: (state) => {
state.value += 1
},
reset: (state) => {
state.value = 0
},
// PayloadAction<number> types the extra data sent with the action
incrementBy: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
// createSlice generated these action creators from the reducers above
export const { increment, reset, incrementBy } = counterSlice.actions
// …and this reducer, which app/store.ts plugged in as state.counter
export default counterSlice.reducerWhy: you never change the store directly — you DISPATCH an action, a plain object describing what happened. The action creators from your slice build those objects for you: calling incrementBy(5) returns { type: 'counter/incrementBy', payload: 5 }. The "payload" is just the data the change needs. Understanding this object is the whole mental model — everything else is convenience on top.
import { increment, incrementBy } from './counterSlice'
// An action creator is just a function that returns a plain object:
increment()
// → { type: 'counter/increment' }
incrementBy(5)
// → { type: 'counter/incrementBy', payload: 5 }
// ▲ which reducer runs ▲ the data it needs
// "Dispatching" one of these objects is how you ask the store to change.
// You won't build these by hand — the slice's action creators do it —
// but knowing an action is just { type, payload } demystifies Redux.Why: components can only reach the store if it's provided above them in the tree, exactly like a Context provider. In Next.js the App Router renders on the server by default, so the Provider has to live in a small Client Component marked 'use client', which you then wrap your app in. Note: this file is the one piece of plumbing you set up once and rarely touch again.
// app/StoreProvider.tsx
'use client' // the Provider uses React context, so it must be a Client Component
import { Provider } from 'react-redux'
import { store } from './store'
export default function StoreProvider({
children,
}: {
children: React.ReactNode
}) {
// Everything inside can now reach the store
return <Provider store={store}>{children}</Provider>
}
// app/layout.tsx — wrap your app once
import StoreProvider from './StoreProvider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<StoreProvider>{children}</StoreProvider>
</body>
</html>
)
}