Firstly, install the dependencies:
npm i @reduxjs/toolkit react-redux
Now we can setup redux store by creating a store.ts under a redux/store
directory:
// src/redux/store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import cartSlice from '../features/Cart/cartSlice';
export const store = configureStore({
reducer: {
cart : cartSlice,
}
});
export type RootState = ReturnType<typeof store.getState>;
useSelector
is a hook provided by React Redux that allows to extract data from the Redux store state. By default, useSelector
doesn’t know the shape of our Redux state, which can lead to type safety issues in TypeScript. Thus we can create a custom hook useAppSelector
that is a typed version of useSelector
.
//src/redux/hooks/hooks.ts
import { TypedUseSelectorHook, useSelector } from 'react-redux';
import { RootState } from '../store/store';
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Now we need to create a Slice for State Management. Slices represent a part of our state, here Cart State, for which we will create cartSlice.ts
.
// src/redux/features/cart/cartSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import { Product } from '../../../types/product';
import { CartState } from '../../../types/cart';
const initialState : CartState = {
products: [],
quantity: 0,
total: 0
};
const cartSlice = createSlice({
name: 'cart',
initialState, // initial cart state
reducers: {
addProduct: (state, action) => {
const item = state.products.find(product => product.id === action.payload.id);
if (item) {
item.quantity = (item.quantity ?? 0) + 1;
state.quantity += 1;
state.total += item.price;
} else {
const { price, quantity = 1 } = action.payload;
state.products.push(action.payload);
state.quantity += quantity;
state.total += price;
}
},
removeProduct: (state, action) => {
const item = state.products.find(product => product.id === action.payload.id);
if (item) {
if (item.quantity === 1) {
state.products = state.products.filter(product => product.id !== action.payload.id);
} else {
item.quantity -= 1;
}
state.quantity -= 1;
state.total -= item.price;
}
},
removeProductFromCart: (state, action) => {
const item = state.products.find(product => product.id === action.payload.id);
if (item) {
state.products = state.products.filter(product => product.id !== action.payload.id);
state.quantity -= (item.quantity ?? 0);
state.total -= item.price * (item.quantity ?? 0);
}
}
}
});
export const { addProduct, removeProduct, removeProductFromCart } = cartSlice.actions;
export default cartSlice.reducer;
To ensure type safety we can use product and cart types:
// src/types/product.ts
export interface Product {
id: number;
ItemName: string;
price: number;
imgUrl: string;
quantity?: number; //quantity property is optional
}
// src/types/cart.ts
import { Product } from "./product";
export interface CartState {
products: Product[];
quantity: number;
total: number;
}
Wrap entire application with the Redux Provider component to make the store accessible throughout the app:
// src/main.tsx
import {Provider} from "react-redux";
import {store} from "./redux/store/store";
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
)
Now we can access and dispatch Actions in components. Here's the StoreItem.tsx
:
// src/components/StoreItem.tsx
export const StoreItem = ({ id, ItemName, price, imgUrl }: StoreItemProps) => {
const dispatch = useDispatch();
const cart = useAppSelector((state) => state.cart);
const itemInCart = cart.products.find(item => item.id === id);
const quantity = itemInCart ? itemInCart.quantity : 0;
const handleAddToCart = () => {
dispatch(addProduct({ id, ItemName, price, imgUrl, quantity: 1 }));
}
return (
<Card className="h-100">
<Card.Img/>
<Card.Body>
<Card.Title>
<span>{ItemName}</span>
<span>{formatCurrency(price)}</span>
</Card.Title>
<div className="mt-auto">
{quantity === 0 ? (
<button className="btn btn-primary" onClick={handleAddToCart}> Add to cart </button>
) : (
<div
className="d-flex align-items-center flex-column"
style={{ gap: ".5rem" }}
>
<div
className="d-flex align-items-center justify-content-center"
style={{ gap: ".5rem" }}
>
<button className="btn btn-primary" onClick={() => dispatch(removeProduct({id}))}>-</button>
<div>
<span className="fs-3">{quantity}</span> in cart
</div>
<button className="btn btn-primary" onClick={() => dispatch(addProduct({id}))}>+</button>
</div>
<button className="btn btn-danger" onClick={() => dispatch(removeProductFromCart({id}))}>Remove</button>
</div>
)}
</div>
</Card.Body>
</Card>
)
}