Since the latest Next.js with App Router now, it actually easier to configure it, but i just want to share my code that is stable on production, how do i configure Redux in Next.js, including for Pages Router.
I want to remind you, i just want to share the code, not really like a documentation-ish.
This configure of Redux is using
- React Redux
- Redux Toolkit
- Redux Persist (LocalStorage)
- Redux-State-Sync-3 (I want to sync the redux cross tabs and windows)
- next-redux-wrapper (Only for Pages Router)
- Create a file
whatever-the-name-you-like.tsx
. We will use the component inlayout.tsx
of app file
'use client'
import {
memo,
} from 'react'
import {
combineReducers,
type Reducer,
} from 'redux'
import {
configureStore as _configureStore,
type Slice,
} from '@reduxjs/toolkit'
import {
Provider,
} from 'react-redux'
import {
persistStore,
persistReducer,
FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER,
} from 'redux-persist'
import createWebStorage from 'redux-persist/lib/storage/createWebStorage'
import {
PersistGate,
} from 'redux-persist/integration/react'
import {
createStateSyncMiddleware,
} from 'redux-state-sync'
import type {
PropsWithChildren,
} from 'react'
function createNoopStorage() {
return {
getItem() {
return Promise.resolve(null);
},
setItem(_key: any, value: any) {
return Promise.resolve(value);
},
removeItem() {
return Promise.resolve();
},
}
}
export function configureStore(
stores: Slice[],
persistentBlacklist?: Slice['name'][],
) {
const
reducers =
stores.reduce((acc: Record<string, Reducer>, slice) => {
acc[slice.name] = slice.reducer
return acc
}, {}),
persistedReducer =
persistReducer(
{
key: 'root',
version: 1,
storage: typeof window !== 'undefined' ? createWebStorage('local') : createNoopStorage(),
blacklist: persistentBlacklist,
},
combineReducers(reducers)
),
configuredStore =
_configureStore({
reducer: persistedReducer,
// https://redux-toolkit.js.org/usage/usage-guide#use-with-redux-persist
middleware: getDefaultMiddleware => {
return getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
})
// Skip this if you don't use the Redux State Sync middleware
.concat( typeof window !== 'undefined' ? [
createStateSyncMiddleware({
blacklist: [PERSIST, REHYDRATE],
})
] : [])
},
})
return {
store: configuredStore,
persistor: persistStore(configuredStore),
}
}
export interface ReduxProviderPropsInterface extends PropsWithChildren {
configuredStore: ReturnType<typeof configureStore>,
}
/**
* I actually do not know, we need to memoize this component or not.
*/
const ReduxProvider = memo(function _(props: ReduxProviderPropsInterface): JSX.Element {
return (
<Provider store={ props.configuredStore.store }>
<PersistGate persistor={ props.configuredStore.persistor } loading={ null }>
{ /**
* PersistGate will render the children in server side if you pass children as function
* https://github.com/vercel/next.js/issues/8240#issuecomment-647699316
*/ }
{ () => props.children }
</PersistGate>
</Provider>
)
})
export default ReduxProvider
- Now, create a folder with name
states
orwhatever
(in src if you are using src folder), and createsample.ts
in that folder to create a Slice with Redux Toolkit, here is the sample
import {
createSlice,
} from '@reduxjs/toolkit'
import type {
PayloadAction,
} from '@reduxjs/toolkit'
export interface SampleStateInterface {
nothing?: 'NOTHING' | 'TESTING',
}
const initialState: SampleStateInterface = {
nothing: 'NOTHING',
}
export const slice = createSlice({
name: 'sample',
initialState,
reducers: {
setNothing(state, action: PayloadAction<SampleStateInterface['nothing']>) {
state.nothing = action.payload
},
}
})
export const {
setNothing,
} = slice.actions
- Still in the
states
folder, create a fileindex.ts
'use client'
import * as SampleStore from './sample'
import {
configureStore,
} from './whatever-the-name-you-like.tsx' // The First File
export const ConfiguredStore = configureStore([
SampleStore.slice,
])
// For the useSelector parameter type. You can see it how i use it after the last step.
export interface StatesInterface {
sample: SampleStore.SampleStateInterface,
}
- The last step, in the
layout.tsx
file, wrap the{ children }
with the component of the first file with theConfiguredStore
import {
Inter,
} from 'next/font/google'
import ReduxProvider from './whatever-the-name-you-like' // the first file
import {
ConfiguredStore,
} from '@/states' // the 'states' or 'whatever' folder
import type {
Metadata,
} from 'next'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Testing',
description: 'Hola',
}
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<ReduxProvider configuredStore={ ConfiguredStore }>
{ children }
</ReduxProvider>
</body>
</html>
)
}
DONE.
You can use the useSelector and useDispatch from React Redux now.
import {
useSelector,
useDispatch,
} from 'react-redux'
import * as SampleState from '@/states/sample'
import type {
StatesInterface,
} from '@/states'
export default function Component(): JSX.Element {
const
dispatch =
useDispatch(),
sample =
useSelector((state: StatesInterface) => state.sample)
const testDispatch = () => {
dispatch( SampleState.setNothing('TESTING') )
}
return (
<div onClick={ testDispatch }>
{ sample }
</div>
)
}
(This example above would get you an Hydration Nextjs Error. You should handle by yourself when to render the content of Redux state that persisted in LocalStorage, this is just for an example sake)
For page router, i use next-redux-wrapper only for optimizing each render of page. You can read the next-redux-wrapper Motivation.
I actually do not use this for advance stuff, like hydrating the state from server. Keep client as client. But, it's up to you.
Now i just want to share how to configure it, same like the App Router example above, with React Redux, Redux Toolkit, Redux Persist, and Redux State Sync. It's not that easy like in App Router to configure the store, i actually learn this in two days work.
- Create a file
whatever-the-name-you-like.tsx
/**
* Redux Toolkit
* https://redux-toolkit.js.org/tutorials/quick-start
*/
/* eslint-disable @typescript-eslint/no-var-requires */
import {
combineReducers,
} from 'redux'
import {
configureStore,
} from '@reduxjs/toolkit'
import {
Provider,
} from 'react-redux'
import {
PersistGate,
} from 'redux-persist/integration/react'
import {
createWrapper,
} from 'next-redux-wrapper'
import {
createStateSyncMiddleware,
initMessageListener,
} from 'redux-state-sync'
import type { Slice } from '@reduxjs/toolkit'
import type { Store, Reducer } from 'redux'
import type { Persistor } from 'redux-persist'
import type { ProviderProps } from 'react-redux'
export function createStoreWrapper(
store: Slice[],
) {
const isServer = typeof window === 'undefined'
const reducers = store.reduce((acc: Record<string, Reducer>, slice) => {
acc[slice.name] = slice.reducer
return acc
}, {})
if(isServer) {
/**
* https://github.com/kirill-konshin/next-redux-wrapper#usage-with-redux-persist
* Skip redux persist usage in server side
*/
const makeStore = () => configureStore({
reducer: reducers,
})
interface MakeStore extends ReturnType<typeof makeStore> {
__persistor?: Persistor,
}
return createWrapper<MakeStore>(makeStore)
} else {
/**
* https://github.com/kirill-konshin/next-redux-wrapper#usage-with-redux-persist
* Use redux persist only in client side
*/
const {
persistStore,
persistReducer,
FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER,
} = require('redux-persist')
const storage = require('redux-persist/lib/storage').default
/**
* https://redux-toolkit.js.org/usage/usage-guide#use-with-redux-persist
*/
const persistedReducer: ReturnType<typeof persistReducer> = persistReducer(
{
key: 'root',
storage,
},
combineReducers(reducers),
)
interface StoreInterface extends Store {
__persistor: Persistor
}
/**
* https://redux-toolkit.js.org/usage/usage-guide#use-with-redux-persist
*/
const configuredStore = configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
})
// Skip this if you don't use Redux State Sync
.concat(typeof window !== 'undefined' ? [
createStateSyncMiddleware({
blacklist: [PERSIST, REHYDRATE],
}),
] : []),
})
const _store: StoreInterface = {
...configuredStore,
__persistor: persistStore(configuredStore),
}
/**
* https://github.com/aohua/redux-state-sync
*/
initMessageListener(_store)
return createWrapper<StoreInterface>(() => _store)
}
}
interface ReduxProviderPropsInterface extends ProviderProps {
children: React.ReactNode,
persistor: Persistor,
}
export function ReduxProvider({
persistor,
...props
}: ReduxProviderPropsInterface): JSX.Element {
return (
<Provider { ...props }>
<PersistGate loading={ null } persistor={ persistor }>
{ /**
* PersistGate will render children in server side if you pass children as function
* https://github.com/vercel/next.js/issues/8240#issuecomment-647699316
*/ }
{ () => props.children }
</PersistGate>
</Provider>
)
}
- Same as
Step 2
in App Router section - Similar like
Step 3
in App Router section, it is just renamed fromConfiguredStore
toConfiguredWrapper
, because it doesn't make sense to name it asstore
, it is actuallywrapper
. But i don't care, you can rename it.
import * as SampleStore from './sample'
import {
createStoreWrapper,
} from './whatever-the-name-you-like.tsx' // The First File
export const ConfiguredWrapper = createStoreWrapper([
SampleStore.slice,
])
export interface StatesInterface {
sample: SampleStore.SampleStateInterface,
}
- Last step, in the
_app.tsx
file
import {
ConfiguredWrapper
} from '@/stores'
import type {
AppProps,
} from 'next/app'
import type {
Persistor,
} from 'redux-persist'
function App({ Component, ...rest }: AppProps) {
// https://github.com/kirill-konshin/next-redux-wrapper#wrapperusewrappedstore
const { store, props } = ConfiguredWrapper.useWrappedStore(rest)
return (
<ReduxProvider store={ store } persistor={ store.__persistor as Persistor } >
<Component { ...props }/>
</ReduxProvider>
)
}
export default App
DONE.