Search code examples
javascriptreactjsreduxredux-toolkit

Localstorage with redux tooklit for a shopping cart on an e-commerce application


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

Solution

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