I've built an e-commerce app and need the cart items to stay constant if a user refreshes the page. I have created a slice in this file where all of the actions are. I was doing some reading on redux-tooklit-persist and it sounds like that might be my solution. I've included the reducer file as well as the file which contains the "AddToCart" function.
import { createSlice } from "@reduxjs/toolkit"
const initialState = {
isCartOpen: false,
cart: [],
items: []
}
export const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
setItems: (state, action) => {
state.items = action.payload
},
// Add item to cart
addToCart: (state, action) => {
state.cart = [...state.cart, action.payload.item]
},
// Remove item from cart
removeFromCart: (state, action) => {
state.cart = state.cart.filter((item) => item.id !== action.payload.id)
},
// Increase count in cart
increaseCount: (state, action) => {
state.cart = state.cart.map((item) => {
if (item.id === action.payload.id) {
item.count++
}
return item
})
},
// Decrease count in cart
decreaseCount: (state, action) => {
state.cart = state.cart.map((item) => {
if (item.id === action.payload.id && item.count > 1) {
item.count--
}
return item
})
},
setIsCartOpen: (state) => {
state.isCartOpen = !state.isCartOpen
}
}
})
//Export
export const {
setItems,
addToCart,
removeFromCart,
increaseCount,
decreaseCount,
setIsCartOpen
} = cartSlice.actions
export default cartSlice.reducer
Here is my Item.jsx
import { useState } from "react"
import { useDispatch } from "react-redux"
import { IconButton, Box, Typography, useTheme, Button } from "@mui/material"
import AddIcon from "@mui/icons-material/Add"
import RemoveIcon from "@mui/icons-material/Remove"
import { shades } from "../theme"
import { addToCart } from "../state"
import { useNavigate } from "react-router-dom"
const Item = ({ item, width }) => {
const navigate = useNavigate()
const dispatch = useDispatch()
const [count, setCount] = useState(1)
const [isHovered, setIsHovered] = useState(false)
const {
palette: { neutral }
} = useTheme()
// Destructure from attributes
const { category, price, name, image } = item.attributes
const {
data: {
attributes: {
formats: {
medium: { url }
}
}
}
} = image
return (
<Box width={width}>
<Box
position="relative"
onMouseOver={() => setIsHovered(true)}
onMouseOut={() => setIsHovered(false)}
>
<img
alt={item.name}
width="300px"
height="400px"
src={`http://localhost:1337${url}`}
onClick={() => navigate(`/item/${item.id}`)}
style={{ cursor: "pointer" }}
/>
<Box
display={isHovered ? "block" : "none"}
position="absolute"
bottom="10%"
left="0"
width="100%"
padding="0 5%"
>
<Box display="flex" justifyContent="space-between">
<Box
display="flex"
alignItems="center"
backgroundColor={shades.neutral[100]}
borderRadius="3px"
>
<IconButton onClick={() => setCount(Math.max(count - 1, 1))}>
<RemoveIcon />
</IconButton>
<Typography color={shades.primary[300]}>
{count}
</Typography>
<IconButton onClick={() => setCount(count + 1)}>
<AddIcon />
</IconButton>
</Box>
<Button
onClick={() => {
dispatch(addToCart({ item: { ...item, count } }))
}}
sx={{
backgroundColor: shades.primary[300],
color: "white"
}}
>
Add to Cart
</Button>
</Box>
</Box>
</Box>
<Box mt="3px">
<Typography variant="subtitle2" color={neutral.dark}>
{category
.replace(/([A-Z])/g, " $1")
.replace(/^./, (str) => str.toUpperCase())
}
</Typography>
<Typography>{name}</Typography>
<Typography fontWeight="bold">${price}</Typography>
</Box>
</Box>
)
}
export default Item
redux-persist
is the standard for react
Redux state persistence.
While you could individually initialize and persist state slices from & to localStorage like Mohammad's answer, this is ill-advised as the act of persisting to localStorage is a side-effect in what is considered to be a pure function.
This would be better abstracted into a custom middleware. The RTK maintainers have made it fairly easy to create middleware listeners with createListenerMiddleware
. You can create a middleware listener that reacts to specific actions, or conditions, to persist the store to localStorage.
I'd suggest just going with redux-persist
. If you are already familiar with react
and redux-toolkit
it's a pretty quick setup/integration.
Example:
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { Provider } from "react-redux";
import { PersistGate } from 'redux-persist/integration/react';
import cartReducer from '../path/to/cartSlice';
const persistConfig = {
key: 'root',
storage,
};
const rootReducer = combineReducers({
...other reducers...
cart: cartReducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
...any middlewares and other configuration properties...
});
const persistor = persistStore(store);
At this point I'll also export a wrapper component that renders the react-redux
Provider
component and redux-persist
's PersistGate
component.
export const PersistedProvider = ({ children }) => (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
</Provider>
);
Now instead of wrapping the App
component with the Provider
component you can wrap with the PersistedProvider
component instead and provide the Redux store to the app and persist the redux state.
import { PersistedProvider } from '../path/to/store';
...
<PersistedProvider>
<App />
</PersistedProvider>