Maximizing the potential of next.js app router with Redux-Persist

After following the official documentation on integrating Redux with Next.js app router, everything seemed to be working smoothly. However, I encountered challenges when attempting to persist the data using redux-persist.

The official Redux docs do not provide an example for this situation. I am unsure of how to handle the fact that I am creating a Redux store per request by utilizing configureStore within a makeStore function (the recommended approach suggested by the official Redux documentation for using Redux with Next.js app router).

I am facing difficulties passing the makeStore function to the persistStore method without encountering type errors:

Argument of type '() => EnhancedStore<{ user: userInterface; } & PersistPartial, UnknownAction, Tuple<[StoreEnhancer<{ dispatch: ThunkDispatch<{ user: userInterface; } & PersistPartial, undefined, UnknownAction>; }>, StoreEnhancer]>>'
is not assignable to parameter of type 'Store<any, UnknownAction, unknown>'

This is my current setup:

store.js:

import {
  persistStore,
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { configureStore, combineReducers } from '@reduxjs/toolkit'
import {userReducer} from "./user/userSlice"

const combinedReducers = combineReducers({
  user: userReducer,
})

const persistedReducer = persistReducer(
  {
      key: 'root',
      storage,
      whitelist: ['user'],
  },
  combinedReducers
)

export const makeStore = () => {
  return configureStore({
    reducer: persistedReducer,
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
            serializableCheck: {
                ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
            },
        }),
  })
}

export const persistor = persistStore(makeStore)

export type AppStore = ReturnType<typeof makeStore>
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']

store provider:

"use client";

import { useRef } from "react";
import { Provider } from "react-redux";
import { makeStore, AppStore } from "../redux/store";

export default function StoreProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const storeRef = useRef<AppStore>();
  if (!storeRef.current) {
    // Create the store instance the first time this renders
    storeRef.current = makeStore();
  }

  return <Provider store={storeRef.current}>{children}</Provider>;
}

layout.js

import type { Metadata } from "next";
import "./globals.css";
import StoreProvider from "./StoreProvider";

export const metadata: Metadata = {
  title: "App",
  description: "App Desription",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <StoreProvider>{children}</StoreProvider>
      </body>
    </html>
  );
}

How can I modify the code to achieve data persistence with Redux considering the requirement of creating the store per request?

Answer №1

Here is how I set up the store.ts:

import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'

import userSlice from './features/login/userSlice'

const persistConfig = {
  key: 'persist',
  storage,
}

const rootReducer = combineReducers({
  user: userSlice,
})

const makeConfiguredStore = () =>
  configureStore({
    reducer: rootReducer,
  })

export const makeStore = () => {
  const isServer = typeof window === 'undefined'
  if (isServer) {
    return makeConfiguredStore()
  } else {
    const persistedReducer = persistReducer(persistConfig, rootReducer)
    let store: any = configureStore({
      reducer: persistedReducer,
    })
    store.__persistor = persistStore(store)
    return store
  }
}

export type AppStore = ReturnType<typeof makeStore>
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']

In Next.js, knowing the server and client mode is crucial as we might not need persistence on the server side.

My approach for StoreProvider.tsx is shown below:

'use client'

import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/store'
import { PersistGate } from 'redux-persist/integration/react'

export default function StoreProvider({
  children,
}: {
  children: React.ReactNode
}) {
  const storeRef = useRef<AppStore>()
  if (!storeRef.current) {
    // Create the store instance the first time this renders
    storeRef.current = makeStore()
  }

  return (
    <Provider store={storeRef.current}>
      <PersistGate loading={null} persistor={storeRef.current.__persistor}>
        {children}
      </PersistGate>
    </Provider>
  )
}

By using a ref to keep track of the store state between renders, you can easily wrap your entire app with the StateProvider component.

A special thanks to this blog here for simplifying the process.

Answer №2

After much research and experimentation, I have come up with a solution that effectively combines RTK, RTK query, and redux-persist while adhering to the documentation. Below is the code snippet:

  1. To ensure proper behavior in both server and client environments, create a storage abstraction as shown below:

./ssr-safe-storage.ts

'use client';

import createWebStorage from 'redux-persist/lib/storage/createWebStorage';

interface NoopStorageReturnType {
  getItem: (_key: any) => Promise<null>
  setItem: (_key: any, value: any) => Promise<any>
  removeItem: (_key: any) => Promise<void>
}

const createNoopStorage = (): NoopStorageReturnType => {
  return {
    getItem(_key: any): Promise<null> {
      return Promise.resolve(null);
    },
    setItem(_key: any, value: any): Promise<any> {
      return Promise.resolve(value);
    },
    removeItem(_key: any): Promise<void> {
      return Promise.resolve();
    },
  };
};

const storage =
  typeof window !== 'undefined'
    ? createWebStorage('local')
     : createNoopStorage();

export default storage;
  1. Add the following configuration file for your store (persistor can also be returned from makeStore function or implemented as shown here):

./store.ts

import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE, persistReducer } from 'redux-persist';
import userReducer from '@/features/user/user-slice';
import { userApi } from '@/features/user/user-api';
import storage from './ssr-safe-storage';

const rootReducer = combineReducers({
  [userApi.reducerPath]: userApi.reducer,
  user: userReducer,
});

export const persistConfig = {
  key: 'root',
  storage,
  whitelist: ['user'],
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export const makeStore = () => {
  return configureStore({
    reducer: persistedReducer,
    devTools: process.env.NODE_ENV !== 'production',
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        serializableCheck: {
          ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
         },
       }).concat([userApi.middleware]),
   });
 };

 export type AppStore = ReturnType<typeof makeStore>;
 export type RootState = ReturnType<AppStore['getState']>;
 export type AppDispatch = AppStore['dispatch'];
  1. Finally, create a store provider to bring all the logic together:

./store-provider.tsx

'use client';

import { Provider } from 'react-redux';
import React, { useRef } from 'react';
import { type Persistor, persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/integration/react';
import { type AppStore, makeStore } from './store';

interface StoreProviderProps {
  children: React.ReactNode
}

const StoreProvider: React.FC<StoreProviderProps> = ({ children }) => {
  const storeRef = useRef<AppStore>();
  const persistorRef = useRef<Persistor>({} as Persistor);
  if (!storeRef.current) {
    storeRef.current = makeStore();
    persistorRef.current = persistStore(storeRef.current);
 }

   return (
     <Provider store={storeRef.current}>
       <PersistGate loading={null} persistor={persistorRef.current}>
         {children}
       </PersistGate>
     </Provider>
   );
};

export default StoreProvider;

Note that there are more TypeScript types included than necessary due to specific ESlint configurations. Additionally, below are the reference links that aided me in creating this setup:

Answer №3

When setting up your StoreProvider component, ensure to include the persistStore function call within it.

"use client";
import { useRef } from "react";
import { Provider } from "react-redux";
import { makeStore, AppStore } from "../lib/store";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";

export default function StoreProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const storeRef = useRef<AppStore>();
  if (!storeRef.current) {
    // Create the store instance the first time this renders
    storeRef.current = makeStore();
  }
  const persistedStore = persistStore(storeRef.current);

  return (
    <Provider store={storeRef.current}>
      <PersistGate loading={null} persistor={persistedStore}>
        {children}
      </PersistGate>
    </Provider>
  );
}

It is important not to forget calling persistStore in store.ts as well.

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import comparisonsReducer from "./slices/comparisonsSlice";
import storage from "redux-persist/lib/storage";
import {
  persistReducer,
  FLUSH,
  REHYDRATE,
  PAUSE,
  PERSIST,
  PURGE,
  REGISTER,
} from "redux-persist";

const persistConfig = {
  key: "root",
  storage,
};

const combinedReducers = combineReducers({
  comparison: comparisonsReducer,
});

const persistedReducer = persistReducer(persistConfig, combinedReducers);

export const makeStore = () => {
  return configureStore({
    reducer: persistedReducer,
    devTools: process.env.NODE_ENV !== "production",
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware({
        serializableCheck: {
          ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
        },
      }),
  });
};

// Infer the type of makeStore
export type AppStore = ReturnType<typeof makeStore>;
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The paths configuration in tsconfig.app.json is not functioning as anticipated

Working with Angular to develop my website has been a rewarding experience. I am currently faced with the task of setting a BASE_API for my project, depending on whether it is in prod or dev> mode. To accomplish this, I have made necessary modifications ...

How to have Angular open a PDF file in a new tab

Currently, I am working on implementing the functionality to open a PDF file in a new tab using Angular 9. The PDF file is received from an API as a blob. However, I have encountered an issue due to the deprecation of window.URL.createObjectURL(blob);. Thi ...

Having trouble displaying the button upon initial load using ngIf

My goal is to display a button when editing an input form. Initially, the button is hidden when the page loads but it should appear once any of the input fields are edited. I have also implemented highlighting for the input box that is being edited. Howeve ...

Http service not found

I am facing a problem with injecting HTTP into my Angular 2 application. Everything was working smoothly a few days ago, but now I am encountering this error: ORIGINAL EXCEPTION: No provider for Http! Here is the code snippet from main.ts: import { pl ...

Avoiding the page from scrolling to the top when the sidebar is opened (NextJS, Typescript, TailwindCSS)

I'm currently working on a website that features a sidebar for smaller screens. While the sidebar functions properly, I would like to keep the background page fixed in place when the sidebar is active so it does not scroll. In my code, I have utilize ...

Using Next.js with Firebase emulators

I've been struggling to configure Firebase's V9 emulators with Next.js, but I keep running into the same error message. See it here: https://i.stack.imgur.com/Uhq0A.png The current version of Firebase I'm using is 9.1.1. This is how my Fir ...

Is there a way to configure ESLint so that it strictly enforces either all imports to be on separate lines or all on a single line?

I am currently using ESLint for TypeScript linting. I want to set up ESLint in a way that requires imports to be either all on separate lines or all on a single line. Example of what is not allowed: import { a, b, c, d } from "letters"; Allo ...

Issue with Angular nested observable & forkJoin failing in one scenario, while it functions correctly in another

UPDATE The simulation for OtherService was incorrect. You can check the detailed explanation on this answer. I am trying to identify the issue in the test file that causes it to behave differently from the component. I am puzzled as to why the next: state ...

Issues resolving the signature of a parameter in a Typescript decorator within vscode

I encountered an error while attempting to decorate a class in my NestJS service. The Typescript code compiles without any issues, but I am facing this problem only in VSCode. Unable to resolve signature of parameter decorator when called as an expression ...

What is the process for assigning a value of a different type in blocking situations?

With Javascript, we have the flexibility to push values that we want to variables easily. However, in TypeScript, how can we block (throw an error) this action? let array: Array<string> = []; array.push(5); console.log(array); Even though my IDE no ...

Using TypeScript, apply an event to every element within an array of elements through iteration

I have written the code snippet below, however I am encountering an issue where every element alerts the index of the last iteration. For instance, if there are 24 items in the elements array, each element will alert "Changed row 23" on change. I underst ...

Utilizing props in styled-components: A beginner's guide

I am trying to pass a URL to a component so that I can use it as the background image of the component. Additionally, I need to check if the URL is empty. Component: <BannerImg/> CSS (styled): `const BannerImg = styled.img` background-image: url( ...

Create a simulated constructor to generate an error

Currently, I am faced with a challenge of writing a test that is expected to fail when trying to instantiate the S3Client object. It seems like Vitest (similar to Jest) replaces the constructor with its own version when mocked, preventing my original const ...

Creating a split hero section view using a combination of absolute/relative CSS techniques, Tailwind, and React

I'm in the process of creating a website using Nextjs, React, and TailwindCSS, and I aim to design a Hero section that resembles the one on the following website. https://i.sstatic.net/tq3zW.png My goal is to: Have a text title and buttons on the l ...

Accessing nested objects within an array using lodash in typescript

After analyzing the structure of my data, I found it to be in this format: {property: ["a","b"], value : "somevalue" , comparison : "somecomparison"} I am looking for a way to transform it into a nested object like so: { "properties": { "a": { ...

Access Azure-Active Directory through cypress tests

Currently, I'm faced with the task of creating automated tests for an application that requires login to Azure Active Directory. These tests are being written using Cypress and TypeScript. In search of a solution, I am seeking advice on how to execute ...

Limit the utilization of toString through a TypeScript interface or type

Here's the interface I'm aiming for: export interface Point { readonly x: number; readonly y: number; readonly toString: never; } I initially expected it to function this way: const p: Point = {x: 4, y: 5}; // This should work fine p.toStr ...

What is the method for getting TypeScript to type check object literals with dynamic keys?

I'm grappling with the concept of how TypeScript handles type checking when utilizing dynamic keys in object literals. Let's consider these two functions that produce a duplicate of an object: type Foo = { a: number; b: number; }; const INIT ...

Exporting modules/namespaces to the window object in TypeScript

I have experience building APIs and applications in ES2015, but I am still learning the best practices for TypeScript. Can someone assist me with this challenge? Suppose I am creating an API/SDK for a shop. The objective is for the user to include my js f ...

Tips for incorporating dynamic URLs in Next.js

In my current project using nextjs, I am dealing with fetching images via an API. Right now, I am receiving the "Full image path" (for example, "https://myurl.com/image/imagename.jpg") without any issue. However, I need to figure out how to fetch the image ...