Author : Smriti Pradhan
Credits : Maximilian Schwarzmüller
- Handling Asynchronous Task with Redux
- Using useEffect with Redux and the Problem with useEffect
- Handling HTTP States and Feedbacks with Redux
- Using an Action Creator Thunk , Fetching Data
- Exploring Redux DevTools
---
Reducer function must be pure, side effect free and synchronous.When we dispatch some actions that would involve side effects like HTTP requests to be sent. Where should we put our code ?
There are two possible places 1.Inside the component (eg - useEffect()) So we dispatch action after sideEffect is done so Redux dont know anything about the side effect.
- Action Creators
Install react-redux with @reduxjs/toolkit
1 . npm i
2 . npm install @reduxjs/toolkit react-redux
After the installation we will setup the store.We are going to make 2 slices. One is for cart and another is for toggling the cart card.
1 . ui-slice.js 2 . cart-slice.js
ui-slice.js
We can write mutating code here because when using Redux Toolkit,we are not really mutating the state,even though it looks like we do,but instead Redux Toolkit will kind of capture this code and use another third party library imer to ensure that this is actually translated to some immutable code which creates a new state object instead of manipulating the existing one.
Note - Reference - Arrays and Objects
let arr = [
{ name:"string 1", value:"hey", other: "that" },
{ name:"string 2", value:"hello", other: "that" }
];
let obj = arr.find(o => o.name === 'string 1');
obj.name = "Smriiti";
console.log(arr);
//{name: "Smriiti", value: "this", oth...}
//{name: "string 2", value: "this", ot...}
Mutation in the original object. This concept of mutation can be used in Redux Toolkit which is prohibited in Redux as Redux Toolkit use a third party library immur internally which does not mutate internally.Behind the scenes , immur handles the object.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
items: [],
totalQuantity: 0,
};
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addToCart(state, action) {
const newItem = action.payload;
state.totalQuantity++;
const existingItem = state.items.find((item) => item.id === newItem.id);
if (!existingItem) {
//If there is no existing item
state.items.push({
id: newItem.id,
name: newItem.title,
price: newItem.price,
quantity: 1,
totalPrice: newItem.price,
});
} else {
existingItem.quantity++;
existingItem.totalPrice = existingItem.totalPrice + newItem.price;
}
},
removeFromCart(state, action) {
const id = action.payload;
const existingItem = state.items.find((item) => item.id === id);
state.totalQuantity--;
if (existingItem.quantity === 1) {
state.items = state.items.filter((item) => item.id !== id);
} else {
existingItem.quantity--;
existingItem.totalPrice = existingItem.totalPrice - existingItem.price;
}
},
},
});
export default cartSlice.reducer;
export const cartActions = cartSlice.actions;
Adding products and displaying it in the cart.
For toggle of Cart Details .
import { createSlice } from "@reduxjs/toolkit";
const initialState = { cartIsVisible: false }; //Cart is visible or not visible
const uiSlice = createSlice({
name: "ui",
initialState,
reducers: {
toggle(state) {
state.cartIsVisible = !state.cartIsVisible;
},
},
});
export default uiSlice.reducer;
export const uiActions = uiSlice.actions;
For dispatching
dispatch(
cartActions.addToCart({
id,
title,
price,
})
);
We want to make an HTTP request to the server and then when our project reload the cart should be in the Screen. As of now whenever we are reloading the application the cart item is lost so we can store the data in the Database and whenever the application reload we can fetch it .
For our Application we will be using Firebase as backend. We can also use Node JS.
Here, in our Application we are using Firebase with no logic and just storing the data.So, we need to do more work in the frontend to handle the cart. We cannnot send a HTTP request inside the reducer. We wrote code in the frontend to handle the cart Information.For example adding the product to cart when it was not there and adding when the product existed. THe same with the removing it. This can also be handled in the backend side where we could do less work in the frontend side but this is not the case here. So, we need to handle everything in the frontend side.
There are two ways to achieve-
- Inside the Component (eg useEffect);
- Action Creator
When there is a choice for where to put our Code. There are Fat Reducers VS Fat Components VS Fat Actions. Two types of code - Side Effect Free Synchronous Code -- Prefer Reducers Asynchronous Code with Side Effect. -- Prefer Action Creator or Components.
.Sync our new state to the server . We can first do work on the frontend that is Redux update its store and then we send a request to the server.
In App.js or Product.js We will listen to changes of the cart using useSelector and then send HTTP request to the server. We will send a PUT request to the server as we will replace the entire cart data.
We face one problem when using useEffect the way we currently do it: It will execute when our app starts.Why is this an issue It's a problem because this will send the initial (i.e. empty) cart to our backend and overwrite any data stored there.
Here we will add a notification component which is a bar at the top with some message like sending request , or something wrong happened .The problem of whenever the App starts it will send the inital cart which is empty.
Adding Notification Component and using with Redux
App.js
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import { useDispatch, useSelector } from "react-redux";
import { useEffect, useState } from "react";
import { uiActions } from "./store/ui-slice";
import Notification from "./components/UI/Notification";
let isInitial = true;
function App() {
const dispatch = useDispatch();
const cartIsVisible = useSelector((state) => state.ui.cartIsVisible);
const cart = useSelector((state) => state.cart);
const notification = useSelector((state) => state.ui.notification);
useEffect(() => {
const sendCartData = async () => {
if (isInitial) {
isInitial = false;
return;
}
dispatch(
uiActions.showNotification({
status: "pending",
title: "Sending Cart Data",
message: "Sending Cart Data",
})
);
const response = await fetch(
"https://react-post-call-default-rtdb.firebaseio.com/cart.json",
{
method: "PUT",
body: JSON.stringify(cart),
}
);
if (!response.ok) {
throw new Error("Sending Cart Data failed");
}
dispatch(
uiActions.showNotification({
status: "success",
title: "Sending Cart Data Success",
message: "Data Sent Successfully",
})
);
};
sendCartData(cart).catch((err) => {
dispatch(
uiActions.showNotification({
status: "error",
title: "Sending Cart Data Failed",
message: "Error in Sending Cart Data",
})
);
});
}, [cart, dispatch]);
return (
<>
{notification && (
<Notification
status={notification.status}
message={notification.message}
title={notification.title}
/>
)}
<Layout>
{cartIsVisible && <Cart />}
<Products />
</Layout>
</>
);
}
export default App;
ui-slice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = { cartIsVisible: false, notification: null }; //Cart is visible or not visible
const uiSlice = createSlice({
name: "ui",
initialState,
reducers: {
toggle(state) {
state.cartIsVisible = !state.cartIsVisible;
},
showNotification(state, action) {
state.notification = {
status: action.payload.status,
title: action.payload.title,
message: action.payload.message,
};
},
},
});
export default uiSlice.reducer;
export const uiActions = uiSlice.actions;