Persist redux value not working with next-redux-wrapper
Describe the bug
I am useing redux-toolkit, redux-persist, next-redux-wrapper.
When saving data to the storage space, it works normally.
But refresh page, the data not persisted....
To Reproduce
package.json
{
"name": "jebs-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "NODE_ENV='development' node server.js",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write \"src/**/*\"",
"local": "nodemon ./server.js localhost 3080"
},
"dependencies": {
"@hookform/resolvers": "^2.8.2",
"@reduxjs/toolkit": "^1.6.2",
"@types/parse-link-header": "^1.0.0",
"@types/react-redux": "^7.1.19",
"axios": "^0.22.0",
"babel-plugin-styled-components": "^1.13.2",
"browser-image-compression": "^1.0.16",
"express": "^4.17.1",
"google-map-react": "^2.1.10",
"next": "11.1.2",
"next-i18next": "^8.9.0",
"next-redux-wrapper": "^7.0.5",
"nodemon": "^2.0.4",
"parse-link-header": "^1.0.1",
"random-id": "^1.0.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-hook-form": "^7.17.2",
"react-hotkeys-hook": "^3.4.3",
"react-phone-input-2": "^2.14.0",
"react-redux": "^7.2.5",
"react-toastify": "^8.0.3",
"redux-persist": "^6.0.0",
"styled-components": "^5.3.1",
"styled-system": "^5.1.5",
"typescript": "4.4.3",
"yup": "^0.32.11"
},
"devDependencies": {
"@types/browser-image-compression": "^1.0.9",
"@types/google-map-react": "^2.1.3",
"@types/react": "17.0.27",
"@types/styled-components": "^5.1.14",
"@types/styled-system": "^5.1.13",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"babel-eslint": "^10.1.0",
"eslint": "7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"http-proxy-middleware": "^2.0.1",
"prettier": "^2.4.1"
}
}
rootReducer.ts
import { CombinedState, combineReducers } from '@reduxjs/toolkit'
import { HYDRATE } from 'next-redux-wrapper'
import { persistReducer } from 'redux-persist'
import storageSession from 'redux-persist/lib/storage/session'
import { KEY_SLICE_ROOT, KEY_SLICE_STORAGE } from '../../common/key'
import authenticationReducer, { AuthState } from './authSlice'
import chatsReducer, { ChatsState } from './chatsSlice'
import progressReducer, { ProgressState } from './progressSlice'
import storageReducer, { StorageState } from './storageSlice'
import userReducer, { UserState } from './userSlice'
const persistConfig = {
key: KEY_SLICE_ROOT,
storage: storageSession,
whitelist: [KEY_SLICE_STORAGE],
}
const rootReducer = (
state: any,
action: any,
): CombinedState<{
progress: ProgressState
authentication: AuthState
chats: ChatsState
storage: StorageState
user: UserState
}> => {
if (action.type === HYDRATE) {
return {
...state,
...action.payload,
}
}
return combineReducers({
progress: progressReducer,
authentication: authenticationReducer,
chats: chatsReducer,
storage: storageReducer,
user: userReducer,
})(state, action)
}
export type RootState = ReturnType<typeof rootReducer>
export default persistReducer(persistConfig, rootReducer)
store.ts
import { Action, configureStore } from '@reduxjs/toolkit'
import { createWrapper } from 'next-redux-wrapper'
import { persistStore } from 'redux-persist'
import { ThunkAction } from 'redux-thunk'
import reducer, { RootState } from './features'
const store = configureStore({
reducer,
devTools: process.env.NODE_ENV !== 'production',
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
})
const makeStore = () => store
export type AppDispatch = typeof store.dispatch
export type AppThunk = ThunkAction<void, RootState, unknown, Action<string>>
export const persistor = persistStore(store) // Nasty hack
// next-redux-wrapper에서 제공하는 createWrapper정의
export const wrapper = createWrapper(makeStore, {
debug: process.env.NODE_ENV !== 'production',
})
export default store
_app.tsx
import { NextPage } from 'next'
import { appWithTranslation } from 'next-i18next'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import 'react-phone-input-2/lib/bootstrap.css'
import { Provider } from 'react-redux'
import 'react-toastify/dist/ReactToastify.css'
import { PersistGate } from 'redux-persist/integration/react'
import { ThemeProvider } from 'styled-components'
import ProgressBar from '../components/ProgressBar'
import Spinner from '../components/Spinner'
import Toast from '../components/Toast'
import store, { persistor, wrapper } from '../store'
import '../styles/globals.css'
const App: NextPage<AppProps> = ({ Component, pageProps }) => {
return (
<>
<Head>
<title>jebs</title>
</Head>
<Provider store={store}>
<PersistGate persistor={persistor} loading={<Spinner />}>
<ThemeProvider
theme={{
breakpoints: ['501px', '769px', '1920px'],
}}
>
<ProgressBar />
<Toast />
<Component {...pageProps} />
</ThemeProvider>
</PersistGate>
</Provider>
</>
)
}
export default wrapper.withRedux(appWithTranslation(App))
storageSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { KEY_SLICE_STORAGE } from '../../common/key'
export interface StorageState {
accessToken: string | null
}
const initialState: StorageState = {
accessToken: null,
}
const storage = createSlice({
name: KEY_SLICE_STORAGE,
initialState,
reducers: {
saveAccessToken(state, action: PayloadAction<string>) {
state.accessToken = action.payload
},
},
})
export const { saveAccessToken } = storage.actions
export default storage.reducer
userSlice.ts
...
export const fetchConfirmPhoneCodeAndValues =
(uid: string, name: string, phone: string, code: string): AppThunk =>
async (dispatch) => {
try {
dispatch(loading())
const { statusCode, message } = await getCheckFindPWPhoneAndValues(
uid,
name,
phone,
code,
)
if (statusCode && statusCode === 200) {
toast.success(MESSAGE_CONFIRM_CODE_SUCCESS)
dispatch(confirmCodeSuccess())
dispatch(
saveAccessToken(
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlZ0Mm1NSXVqYkFoT0dHOC0iLCJpYXQiOjE2MzU5MTQ2E',
),
)
} else {
throw new Error(message ?? '')
}
} catch (err) {
if (err) {
const { message } = err as ErrorState
toast.error(message)
} else {
toast.error(ERROR_BASIC_MESSAGE)
}
}
}
...
Expected behavior
Even if the page is switched or refreshed, the value of accsses_token in storage must be maintained.
Screenshots
Before refresh or switch page

After refresh or switch page

Desktop (please complete the following information):
- OS: MacOS version 11.4
- Browser : chrome
i got the same problem
Have a look at https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist
Have a look at https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist
I was hoping that repo had the solution, as it seemed promising. Unfortunately it doesn't work, as the screen will still be white if javascript is disabled.
Also created an issue here: https://github.com/vercel/next.js/discussions/31884
It's amazing how such a single feature turns out to be so challenging. Oh well, hence my username.
Has this been fixed ? , i'm having the same issue
did you manage to resolve the error?
I got the same error, any solution?
I gto the same error, did you find the solution?
// reducers.ts
import { combineReducers, CombinedState, AnyAction } from '@reduxjs/toolkit'
import storage from 'redux-persist/lib/storage'
import { persistReducer } from 'redux-persist'
import { HYDRATE } from 'next-redux-wrapper'
import storageReducer, {
IStorage,
storageSlice,
} from 'src/core/redux/storageSlice'
import statusReducer, {
IStatusSlice,
statusSlice,
} from 'src/core/redux/statusSlice'
import authReducer, { IAuthSlice, authSlice } from 'src/core/redux/authSlice'
import userReducer, {
IMemberSlice,
memberSlice,
} from 'src/core/redux/memberSlice'
import screenReducer, {
IScreenSlice,
screenSlice,
} from 'src/core/redux/screenSlice'
import examReducer, {
examSlice,
IExamSlice,
} from 'src/core/redux/contents/examSlice'
import lessonReducer, {
ILessonSlice,
lessonSlice,
} from 'src/core/redux/contents/lessonSlice'
import modalReducer, {
IModalSlice,
modalSlice,
} from 'src/core/redux/modalSlice'
import courseReducer, {
ICourseSlice,
courseSlice,
} from 'src/core/redux/course/courseSlice'
import courseModelReducer, {
ICourseModelSlice,
courseModelSlice,
} from 'src/core/redux/course/courseModelSlice'
import courseLessonReducer, {
ILessonSlice as ICourseLessonSlice,
lessonSlice as courseLessonSlice,
} from 'src/core/redux/course/lessonSlice'
import todoReducer, {
IToDoSlice,
todoSlice,
} from 'src/core/redux/course/todoSlice'
import boostUpReducer, {
IBoostUpSlice,
boostUpSlice,
} from 'src/core/redux/course/boostUpSlice'
import completionReducer, {
ICompletionSlice,
completionSlice,
} from 'src/core/redux/course/completionSlice'
import academicReducer, {
IAcademicSlice,
academicSlice,
} from 'src/core/redux/academicSlice'
import inquiryReducer, {
IInquirySlice,
inquirySlice,
} from 'src/core/redux/cs/inquirySlice'
import counsellingReducer, {
ICounsellingSlice,
counsellingSlice,
} from 'src/core/redux/cs/counsellingSlice'
import searchReducer, {
ISearchSlice,
searchSlice,
} from 'src/core/redux/searchSlice'
import productReducer, {
IProductSlice,
productSlice,
} from 'src/core/redux/productSlice'
import fileReducer, { IFileSlice, fileSlice } from 'src/core/redux/fileSlice'
import jebsOnReducer, {
IJebsOnSlice,
jebsOnSlice,
} from 'src/core/redux/contents/jebsOnSlice'
import jebsPlayReducer, {
IJebsPlaySlice,
jebsPlaySlice,
} from 'src/core/redux/contents/jebsPlaySlice'
import certificationReducer, {
ICertificationSlice,
certificationSlice,
} from 'src/core/redux/contents/certificationSlice'
import surveyReducer, {
ISurveySlice,
surveySlice,
} from 'src/core/redux/contents/surveySlice'
import orderReducer, {
IOrderSlice,
orderSlice,
} from 'src/core/redux/order/orderSlice'
import pointReducer, {
IPointSlice,
pointSlice,
} from 'src/core/redux/order/pointSlice'
export interface IReducers {
[storageSlice.name]: IStorage
[statusSlice.name]: IStatusSlice
[authSlice.name]: IAuthSlice
[memberSlice.name]: IMemberSlice
[screenSlice.name]: IScreenSlice
[examSlice.name]: IExamSlice
[lessonSlice.name]: ILessonSlice
[modalSlice.name]: IModalSlice
[courseSlice.name]: ICourseSlice
[courseModelSlice.name]: ICourseModelSlice
[courseLessonSlice.name]: ICourseLessonSlice
[academicSlice.name]: IAcademicSlice
[todoSlice.name]: IToDoSlice
[boostUpSlice.name]: IBoostUpSlice
[completionSlice.name]: ICompletionSlice
[inquirySlice.name]: IInquirySlice
[searchSlice.name]: ISearchSlice
[counsellingSlice.name]: ICounsellingSlice
[fileSlice.name]: IFileSlice
[jebsOnSlice.name]: IJebsOnSlice
[jebsPlaySlice.name]: IJebsPlaySlice
[certificationSlice.name]: ICertificationSlice
[productSlice.name]: IProductSlice
[surveySlice.name]: ISurveySlice
[orderSlice.name]: IOrderSlice
[pointSlice.name]: IPointSlice
}
const combinedReducers = combineReducers({
// Add new slice here!
[storageSlice.name]: storageReducer,
[statusSlice.name]: statusReducer,
[authSlice.name]: authReducer,
[memberSlice.name]: userReducer,
[screenSlice.name]: screenReducer,
[examSlice.name]: examReducer,
[lessonSlice.name]: lessonReducer,
[modalSlice.name]: modalReducer,
[courseSlice.name]: courseReducer,
[courseModelSlice.name]: courseModelReducer,
[courseLessonSlice.name]: courseLessonReducer,
[academicSlice.name]: academicReducer,
[todoSlice.name]: todoReducer,
[boostUpSlice.name]: boostUpReducer,
[completionSlice.name]: completionReducer,
[inquirySlice.name]: inquiryReducer,
[counsellingSlice.name]: counsellingReducer,
[productSlice.name]: productReducer,
[searchSlice.name]: searchReducer,
[fileSlice.name]: fileReducer,
[jebsOnSlice.name]: jebsOnReducer,
[jebsPlaySlice.name]: jebsPlayReducer,
[certificationSlice.name]: certificationReducer,
[surveySlice.name]: surveyReducer,
[orderSlice.name]: orderReducer,
[pointSlice.name]: pointReducer,
})
export const rootReducers = (
state: any,
action: AnyAction,
): CombinedState<IReducers> => {
if (action.type === HYDRATE) {
return {
...state,
...action.payload,
}
}
return combinedReducers(state, action)
}
export const persistConfig = {
key: 'jebs-admin',
storage,
whitelist: [storageSlice.name],
}
export default persistReducer(persistConfig, rootReducers)
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'
import { createWrapper } from 'next-redux-wrapper'
import { persistStore } from 'redux-persist'
import persistedReducer, {
IReducers,
rootReducers,
} from 'src/core/redux/reducers'
export let persistor: any
const makeConfiguredStore = (reducer: any) =>
configureStore({
reducer,
devTools: process.env.NODE_ENV !== 'production',
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
})
const makeStore = () => {
const isServer = typeof window === 'undefined'
if (isServer) {
return makeConfiguredStore(rootReducers)
} else {
const store = makeConfiguredStore(persistedReducer)
persistor = persistStore(store)
return { ...store, persistor }
}
}
const store = makeStore()
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type AppState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
IReducers,
unknown,
Action<string>
>
export const wrapper = createWrapper<AppStore>(makeStore, {
debug: process.env.NODE_ENV !== 'production',
// * Error: Error serializing `.initialState.status.error` returned from `getStaticProps` in "/*".
// * Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value.
// * 오류로 내용 추가
serializeState: (state) => JSON.stringify(state),
deserializeState: (state) => JSON.parse(state),
})
export default store