I'm building an online store using React with Redux-Persist. I have an idea to store the user's cart in local storage, creating a separate entry in the local storage for each user (I think it would be more convenient). However, I'm not sure how to switch Redux-Persist to the right setup after changing the user. Currently, I have a general entry with the key 'basket'. I'd like to have 'basket/userId' for each user.
Here's my code:
// store.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE, persistReducer, persistStore } from "redux-persist";
import storage from "redux-persist/lib/storage";
import basketSlice from './slices/basket/basket.slice';
import categoriesSlice from "./slices/category.slice";
import ordersSlice from './slices/orders.slice';
import userSlice from "./slices/user/user.slice";
const rootReducer = combineReducers({
basket: basketSlice,
orders: ordersSlice,
categories: categoriesSlice,
user: userSlice
});
const persistConfig = {
key: `basket`, // I want "basket/userId" for each user
storage,
whitelist: ['basket'],
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE],
},
});
},
});
export const persistor = persistStore(store);
export type RootType = ReturnType<typeof rootReducer>;
// user.slice.ts
import { getValueFromLocalStorage } from "@/functions/getValueFromLocalStorage";
import { createSlice } from '@reduxjs/toolkit';
import { checkAuth, auth, logout } from "./user.actions";
import { IInitialState } from "./user.interface";
const initialState: IInitialState = {
user: getValueFromLocalStorage('user'),
isLoading: false
}
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(auth.pending, (state) => {
state.isLoading = true;
})
.addCase(auth.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload.user;
})
.addCase(auth.rejected, (state) => {
state.isLoading = false;
state.user = null;
})
.addCase(logout.fulfilled, (state) => {
state.isLoading = false;
state.user = null;
})
.addCase(checkAuth.fulfilled, (state, action) => {
state.user = action.payload.user;
})
}
});
export default userSlice.reducer;
// user.actions.ts
import { errorCatch } from '@/api/api.helper';
import { AuthType } from '@/components/screens/Auth/auth.types';
import { removeFromStorage } from '@/services/auth/auth.helper';
import AuthService from '@/services/auth/auth.service';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { IAuthResponse, IEmailPassword } from './user.interface';
export const auth = createAsyncThunk<
IAuthResponse,
{ type: AuthType; data: IEmailPassword }
>(`auth`, async ({ type, data }, thunkApi) => {
try {
const response = await AuthService.main(type, data);
return response;
} catch (error) {
return thunkApi.rejectWithValue(error);
}
});
export const logout = createAsyncThunk('auth/logout', async () =>
removeFromStorage()
);
export const checkAuth = createAsyncThunk<IAuthResponse>(
'auth/check-auth',
async (_, thunkApi) => {
try {
const response = await AuthService.getNewTokens();
return response.data;
} catch (error) {
if (errorCatch(error) === 'jwt expired') thunkApi.dispatch(logout());
return thunkApi.rejectWithValue(error);
}
}
);
// App.tsx
import { Toaster } from '@/components/ui/toaster';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import Paths from './components/routes/Routes';
import { persistor, store } from './store/store';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Paths />
<Toaster />
</PersistGate>
<ReactQueryDevtools initialIsOpen={false} />
</Provider>
</QueryClientProvider>
);
}
export default App;
// basketSlice.ts
import { IBasketItem } from "@/interfaces/basket.interface";
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { IBasketSlice } from "./basket.types";
const initialState: IBasketSlice = {
products: null
};
const basketSlice = createSlice({
name: 'basket',
initialState,
reducers: {
addProductToBasket(state, action: PayloadAction<IBasketItem>) {
if (state.products) state.products.push(action.payload);
else state.products = [action.payload];
},
deleteProductFromBasket(state, action: PayloadAction<number>) {
if (!state.products) return;
state.products = state.products.filter(product => product.id !== action.payload);
},
}
});
export const basketActions = basketSlice.actions;
export default basketSlice.reducer;
P.S.
Redux-Persist expects the storage key to be static. The problem you have is that you could use a dynamically computed key, but the key value you want/need is stored in the Redux store you are persisting. Since the user baskets are the only values being persisted I would suggest a more manual and localized persistence of the basket state where you can dynamically persist multiple baskets.
Add an id
property to the IBasketSlice
interface:
interface IBasketSlice {
id: string;
products: Product[] | null;
}
Update the basket slice to respond to auth changes to set a local id for the current basket you want to interact with, and to persist/load the products array to/from localStorage:
import { createSlice } from '@reduxjs/toolkit';
import { auth, checkAuth, logout } from '../path/to/user.actions.ts';
const initialState: IBasketSlice = {
id: "",
products: null,
};
const basketSlice = createSlice({
name: 'basket',
initialState,
reducers: {
addProductToBasket(state, action: PayloadAction<IBasketItem>) {
if (state.products) {
state.products.push(action.payload);
} else {
state.products = [action.payload];
}
if (state.id) {
localStorage.setItem(
`basket/${state.id}`,
JSON.stringify(state.products)
);
}
},
deleteProductFromBasket(state, action: PayloadAction<number>) {
if (!state.products) return;
state.products = state.products.filter(
product => product.id !== action.payload
);
if (state.id) {
localStorage.setItem(
`basket/${state.id}`,
JSON.stringify(state.products)
);
}
},
},
extraReducers: (builder) => {
const resetBasket = (state) => {
state.id ="";
state.products = null;
};
const setBasket = (state, action) => {
const { id } = action.payload.user;
state.id = id;
state.products =
JSON.parse(localStorage.getItem(`basket/${id}`)) ?? [] as Product[];
};
builder
.addCase(auth.fulfilled, setBasket)
.addCase(checkAuth.fulfilled, setBasket)
.addCase(auth.rejected, resetBasket)
.addCase(logout.fulfilled, resetBasket);
}
});
I haven't tested this in a running sandbox or anything so please do double-check the Typescript typings/syntax/etc since I'm only using a plain text editor here.