next-redux-wrapper icon indicating copy to clipboard operation
next-redux-wrapper copied to clipboard

Persist redux value not working with next-redux-wrapper

Open flexboni opened this issue 4 years ago • 5 comments

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

image

After refresh or switch page

image

Desktop (please complete the following information):

  • OS: MacOS version 11.4
  • Browser : chrome

flexboni avatar Nov 03 '21 08:11 flexboni

i got the same problem

masterambi avatar Nov 04 '21 12:11 masterambi

Have a look at https://github.com/fazlulkarimweb/with-next-redux-wrapper-redux-persist

simplenotezy avatar Nov 30 '21 20:11 simplenotezy

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.

simplenotezy avatar Nov 30 '21 21:11 simplenotezy

Has this been fixed ? , i'm having the same issue

DevYemi avatar Jul 15 '22 15:07 DevYemi

did you manage to resolve the error?

vizardkill avatar Sep 12 '22 23:09 vizardkill

I got the same error, any solution?

mnzsss avatar Nov 15 '22 20:11 mnzsss

I gto the same error, did you find the solution?

massimo1220 avatar May 26 '23 15:05 massimo1220

// 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

flexboni avatar Jan 11 '24 23:01 flexboni