Skip to content

Instantly share code, notes, and snippets.

@RakaDoank
Last active July 10, 2023 09:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save RakaDoank/c62919106ad1b0649ef784b52d8941ae to your computer and use it in GitHub Desktop.
Save RakaDoank/c62919106ad1b0649ef784b52d8941ae to your computer and use it in GitHub Desktop.
How do i configure Redux in Next.js

How do i configure Redux in Next.js

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

App Router

  1. Create a file whatever-the-name-you-like.tsx. We will use the component in layout.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
  1. Now, create a folder with name states or whatever (in src if you are using src folder), and create sample.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
  1. Still in the states folder, create a file index.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,
}
  1. The last step, in the layout.tsx file, wrap the { children } with the component of the first file with the ConfiguredStore
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)

Page Router

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.

  1. 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>
  )

}
  1. Same as Step 2 in App Router section
  2. Similar like Step 3 in App Router section, it is just renamed from ConfiguredStore to ConfiguredWrapper, because it doesn't make sense to name it as store, it is actually wrapper. 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,
}
  1. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment