reactjstypescriptreduxredux-toolkitredux-persist

TypeScript Redux lost state upon refreshing page


redux/reducer/cartReducer.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { CartReducerInitialState } from "../../types/reducer-types";
import { CartItem, ShippingInfo } from "../../types/types";

const initialState: CartReducerInitialState = {
    loading: false,
    cartItems: [],
    subtotal: 0,
    tax: 0,
    shippingCharges: 0,
    discount: 0,
    total: 0,
    shippingInfo: {
        address: "",
        city: "",
        state: "",
        country: "",
        pinCode: "",
    },
};

export const cartReducer = createSlice({
  name: "cartReducer",
  initialState,
  reducers: {
    addToCart: (state, action: PayloadAction<CartItem>) => {
      state.loading = true;

      const index = state.cartItems.findIndex(
        (i) => i.productId === action.payload.productId
      );

      if (index !== -1) state.cartItems[index] = action.payload;
      else state.cartItems.push(action.payload);
      state.loading = false;
    },

    removeCartItem: (state, action: PayloadAction<string>) => {
      state.loading = true;
      state.cartItems = state.cartItems.filter(
        (i) => i.productId !== action.payload
      );
      state.loading = false;
    }
  },
});

export const {
  addToCart,
  removeCartItem
} = cartReducer.actions;

redux/store.ts

import { configureStore } from "@reduxjs/toolkit";
import { userAPI } from "./api/userAPI";
import { userReducer } from "./reducer/userReducer";
import { productAPI } from "./api/productAPI";
import { cartReducer } from "./reducer/cartReducer";
import { persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import {combineReducers} from "@reduxjs/toolkit"; 

export const server = import.meta.env.VITE_SERVER;

const reducers = combineReducers({
    [userAPI.reducerPath]: userAPI.reducer,
    [productAPI.reducerPath]: productAPI.reducer,
    [userReducer.name]: userReducer.reducer,
    [cartReducer.name]: cartReducer.reducer           
});

const persistConfig = {
    key: 'root',
    storage,
    whitelist: ["cartReducer"]
};

const persistedReducer = persistReducer(persistConfig, reducers)

export const store = configureStore({
    reducer: persistedReducer,
    middleware: (getDefaultMiddleware) => 
        getDefaultMiddleware().concat([
            userAPI.middleware,
            productAPI.middleware
        ]),
});

export type RootState = ReturnType<typeof store.getState>

pages/Home.tsx

 const { cartItems, subtotal, tax, total, shippingCharges, discount } = useSelector((state: { cartReducer: CartReducerInitialState }) => state.cartReducer)
    
    const { data, isError, isLoading } = useLatestProductsQuery("")
    // console.log(data)
    console.log(cartItems)

    const dispatch = useDispatch()

    const addToCartHandler = (cartItem: CartItem) => {
        if(cartItem.stock < 1) return toast.error("Out of Stock")
        dispatch(addToCart(cartItem))
        toast.success("Item added to cart")
    }
     {
                            data?.products.map((i) => (
                                <ProductCard key={i._id} productId={i._id} name={i.name} price={i.price}
                                category={i.category} stock={i.stock} handler={addToCartHandler} photo={i.photo} />
                            ))
                        }

components/ProductCard.tsx

type ProductProps = {
    productId: string
    photo: string
    name: string
    category: string 
    price: number
    stock: number
    handler: (cartItem: CartItem) => string | undefined
}

const ProductCard = ({
    productId,
    photo,
    name,
    category,
    price,
    stock,
    handler
}: ProductProps) => {
    return (
        <div className="product-card">
            <h3 className="category">{category.toUpperCase()}</h3>
            <img src={`${server}/${photo}`} alt={name} />
            <p>{name}</p>
            <span>HK$ {price}</span>
            <div>
                <button onClick={() => handler({
                    productId,
                    price,
                    name,
                    category,
                    photo,
                    stock,
                    quantity: 1
                })}>
                    <FaPlus />
                </button>

            </div>
        </div>
    )
}

types/types.ts

export type CartItem = {
    productId: string
    photo: string
    name: string
    category: string
    price: number
    quantity: number
    stock: number
}

In this project, I would like to maintain the state of the cart that has been added when the user clicked on the add button. Even I registered the store correctly with the reducer, the cartReducer could not maintain the items added after clicking the button. Till now I tried to add redux-persist but the state still could not be maintained when I click the add button and refresh. I had tried numerous times by following the documentation in redux but still stuck here so can anyone provide help to solve the issue?


Solution

  • You appear to have implemented about half of the persistence setup. What is missing is creating a persistor object and possibly the PersistGate.

    import { configureStore, combineReducers } from "@reduxjs/toolkit";
    import {
      persistStore, // <-- add import
      persistReducer,
      FLUSH, // <-- import actions to ignore in serializability check
      REHYDRATE,
      PAUSE,
      PERSIST,
      PURGE,
      REGISTER,
    } from 'redux-persist';
    import storage from 'redux-persist/lib/storage';
    
    import { userAPI } from "./api/userAPI";
    import { userReducer } from "./reducer/userReducer";
    import { productAPI } from "./api/productAPI";
    import { cartReducer } from "./reducer/cartReducer";
    
    export const server = import.meta.env.VITE_SERVER;
    
    const reducers = combineReducers({
      [userAPI.reducerPath]: userAPI.reducer,
      [productAPI.reducerPath]: productAPI.reducer,
      [userReducer.name]: userReducer.reducer,
      [cartReducer.name]: cartReducer.reducer           
    });
    
    const persistConfig = {
      key: 'root',
      storage,
      whitelist: [cartReducer.name]
    };
    
    const persistedReducer = persistReducer(persistConfig, reducers)
    
    export const store = configureStore({
      reducer: persistedReducer,
      middleware: (getDefaultMiddleware) => 
        getDefaultMiddleware({
          serializableCheck: { // <-- configure serializability middleware
            ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
          },
        }).concat([
          userAPI.middleware,
          productAPI.middleware
        ]),
    });
    
    export const persistor = persistStore(store); // <-- export persistor
    
    export type RootState = ReturnType<typeof store.getState>
    

    Then in the UI ensure you are wrapping the app with the PersistGate under the Redux Provider component.

    Example:

    import { PersistGate } from 'redux-persist/integration/react';
    import { store, persistor } from './path/to/redux/store';
    
    const AppRoot = () => {
      return (
        <Provider store={store}>
          <PersistGate loading={null} persistor={persistor}>
            <App />
          </PersistGate>
        </Provider>
      );
    };