Firebase data changes state but does not render component - reactjs

I am trying to attatch a listener to the chatRoomIds subcollection for a specific user from the User collection.After retriving the chatroomIds for the specific users I have to loop through each to get the specific chatRoom document from the chatRoomIds array in which each element is an object containing chatRoomId that will be used to query for the specific chatRoom.The problem is that the state is working correctly but does not render the Messaage Card component unless I toggle another state in the Message component in the react dev tools on the messages link again after everything loads.
function Message() {
const [currentUser] = useAuthState(auth)
const [{user}] = useStateValue()
const {show, setNotificationPopup, setShow} = useChat();
const [chats, setChats] = useState([])
const [loading, setLoading] = useState(true)
const [chatRoomArraySnap, error] = useCollection(db.collection("Users").doc(user?.uid).collection('ChatRoomIds'))
const chatList = []
const {setLoader, loader} = useLoader();
let chatRoomIds = []
let params = useParams();
const messageId = params.id;
const userObj = {
email: user?.email,
objId: user?.uid,
userName: user?.displayName
};
useEffect(() => {
{
params.id ? setShow(true)
:
setShow(false)
}
}, [])
useEffect(() =>{
// chatList = []
if(user.uid){
db.collection("Users").doc(user.uid).collection('ChatRoomIds').get()
.then(
snapshot => {
snapshot.docs.map((each) => {
// chatRoomIds.push(each.data())
db.collection('ChatRooms').where("chatRoomId", "==", each.data().id).orderBy('dateLastUpdated','desc').onSnapshot(snapshot => {
snapshot.docs.forEach(doc => {
chats.push(doc.data())
console.log(doc.data())
})
// setChats(chatList)
setLoading(false)
// snapshot.docChanges().forEach((change) => {
// if (change.type === "added") {
// console.log("New : ", change.doc.data());
// chatList.push(change.doc.data())
//
// }
// if (change.type === "modified") {
// console.log("Modified : ", change.doc.data());
// setNotificationPopup(change.doc.data())
//
// }
// if (change.type === "removed") {
// console.log("Removed : ", change.doc.data());
// }
// })
})
})
console.log(chatRoomIds)
// chatRoomIds?.map((each) => {
// db.collection('ChatRooms').where("chatRoomId", "==", each.id).orderBy('dateLastUpdated','desc').onSnapshot(snapshot => {
// snapshot.docs.forEach(doc => {
// chatList.push(doc.data())
// console.log(doc.data())
// })
//
// setChats(chatList)
// setLoading(false)
//
//
//
//
//
// // snapshot.docChanges().forEach((change) => {
// // if (change.type === "added") {
// // console.log("New : ", change.doc.data());
// // chatList.push(change.doc.data())
// //
// // }
// // if (change.type === "modified") {
// // console.log("Modified : ", change.doc.data());
// // setNotificationPopup(change.doc.data())
// //
// // }
// // if (change.type === "removed") {
// // console.log("Removed : ", change.doc.data());
// // }
// // })
//
//
// })
// })
}
)
console.log(chatList)
// if (chatRoomArraySnap) {
//
// chatRoomArraySnap.docs.map((each) => {
// chatRoomIds.push(each.data())
// })
// // chatList=[]
// // setChats([])
//
// chatRoomIds.map((each) => {
// db.collection('ChatRooms').where("chatRoomId", "==", each.id).orderBy('dateLastUpdated','desc').onSnapshot(snapshot => {
// snapshot.docs.forEach(doc => {
// chatList.push(doc.data())
// })
//
//
//
//
//
//
// // snapshot.docChanges().forEach((change) => {
// // if (change.type === "added") {
// // console.log("New : ", change.doc.data());
// // chatList.push(change.doc.data())
// //
// // }
// // if (change.type === "modified") {
// // console.log("Modified : ", change.doc.data());
// // setNotificationPopup(change.doc.data())
// //
// // }
// // if (change.type === "removed") {
// // console.log("Removed : ", change.doc.data());
// // }
// // })
//
//
// })
// })
// setChats(chatList)
// console.log(chatList)
//
// }
}
},[])
return (
<>
<Header/>
<div className='container'>
<div className='message '>
<div className='row'>
<div className='col-md-4 col-lg-4 col-sm-12 lg-view'>
<div className=' pt-3 user-list-section'>
<div className='lg-view'>
<h5 className='text-light ml-5 mb-5`'>Messages</h5>
<div className={`d-flex align-items-center`}>
<Search functionHandler={handleSearchChat} props={'#13161A'}/>
<CreateGroupBtn/>
</div>
</div>
<div className='user-list'>
{!loading && chats ? chats.map(chat => {
// console.log(chat.data());
return (<>
<MessageCard key={chat.chatRoomId}
id={chat.chatRoomId} chats={chat}/>
</>
)
}) : <></>}
</div>
</div>
</div>
{
!show ? <>
<div className='sm-view w-100 pl-3 pr-3 '>
<div className=' w-100 d-flex justify-content-center pt-4'>
<div className='flex-grow-1'>
<h4 className='text-light'>Messages</h4>
<p>Talk with your friends</p>
</div>
<div className="search-container flex-grow-1 ">
<div className='search d-flex float-right'>
<CreateGroupBtn/>
</div>
</div>
</div>
<Search functionHandler={handleSearchChat}/>
</div>
<div className='col-md-4 col-lg-4 col-sm-12 sm-view'>
<div className=' pt-3 user-list-section'>
<div className='lg-view'>
<h5 className='text-light ml-5 mb-5`'>Messages</h5>
<div className={`d-flex align-items-center`}>
<Search functionHandler={handleSearchChat} props={'#13161A'}/>
<CreateGroupBtn/>
</div>
</div>
<div className='user-list'>
{!loading && chats !== undefined && chats !== null && chats ? chats.map(chat => {
// console.log(chat.data());
return (<>
<MessageCard key={chat.chatRoomId}
id={chat.chatRoomId} chats={chat}/>
</>
)
}) : <></>}
</div>
</div>
</div>
</>
:
<>
<div className='col-md-8 p-0 col-lg-8 col-sm-12'>
{
messageId && currentUser.email && <MessageWindow/>
}
</div>
</>
}
</div>
</div>
</div>
{!show && <MobileNavbar/>}
</>
);
}
///Working Code
function Message() {
const [currentUser] = useAuthState(auth)
const [{user}] = useStateValue()
const {show, setShow} = useChat();
const [chats, setChats] = useState([])
const [loading, setLoading] = useState(true)
let params = useParams();
const messageId = params.id;
useEffect(() => {
{
params.id ? setShow(true)
:
setShow(false)
}
}, [])
useEffect(() => {
if (user.uid) {
db.collection("Users")
.doc(user.uid)
.collection("ChatRoomIds")
.get()
.then((snapshot) => {
snapshot.docs.map((each) => {
db.collection("ChatRooms")
.where("chatRoomId", "==", each.data().id)
.orderBy("dateLastUpdated", "desc")
.onSnapshot((snapshot) => {
console.log(snapshot.docs.map((doc) => doc.data()))
setChats((chats) =>
chats.concat(snapshot.docs.map((doc) => doc.data()))
);
setLoading(false)
});
});
});
}
}, []);
return (
<>
<Header/>
<div className='container'>
<div className='message '>
<div className='row'>
<div className='col-md-4 col-lg-4 col-sm-12 lg-view'>
<div className=' pt-3 user-list-section'>
<div className='lg-view'>
<h5 className='text-light ml-5 mb-5`'>Messages</h5>
<div className={`d-flex align-items-center`}>
<Search functionHandler={handleSearchChat} props={'#13161A'}/>
<CreateGroupBtn/>
</div>
</div>
<div className='user-list'>
{/*{!loading && chats ? chats.map(chat => {*/}
{/* // console.log(chat.data());*/}
{/* return (<>*/}
{/* <MessageCard key={chat.chatRoomId}*/}
{/* id={chat.chatRoomId} chats={chat}/>*/}
{/* </>*/}
{/* )*/}
{/*}) : <></>}*/}
</div>
</div>
</div>
{
!show ? <>
<div className='sm-view w-100 pl-3 pr-3 '>
<div className=' w-100 d-flex justify-content-center pt-4'>
<div className='flex-grow-1'>
<h4 className='text-light'>Messages</h4>
<p>Talk with your friends</p>
</div>
<div className="search-container flex-grow-1 ">
<div className='search d-flex float-right'>
<CreateGroupBtn/>
</div>
</div>
</div>
<Search functionHandler={handleSearchChat}/>
</div>
<div className='col-md-4 col-lg-4 col-sm-12 sm-view'>
<div className=' pt-3 user-list-section'>
<div className='lg-view'>
<h5 className='text-light ml-5 mb-5`'>Messages</h5>
<div className={`d-flex align-items-center`}>
<Search functionHandler={handleSearchChat} props={'#13161A'}/>
<CreateGroupBtn/>
</div>
</div>
<div className='user-list'>
{!loading && chats ? chats.map(chat => {
return (<>
<MessageCard key={chat.chatRoomId}
id={chat.chatRoomId} chats={chat}/>
</>
)
}) : <></>}
</div>
</div>
</div>
</>
:
<>
<div className='col-md-8 p-0 col-lg-8 col-sm-12'>
{
messageId && currentUser.email && <MessageWindow/>
}
</div>
</>
}
</div>
</div>
</div>
{!show && <MobileNavbar/>}
</>
);
}
export default Message;

You've a lot of commented out code, but what isn't commented out has a glaring issue, state mutation. You are pushing directly into the chats state array. This is why you have to toggle/update some other state in order to trigger the component to rerender and expose the mutation.
You should properly enqueue these chat updates. On the last/innermost snapshot you should use a functional state update to update from the previous state and because you are working in a loop, and concatenate an array of new chat documents.
Array.prototype.concat
The concat() method is used to merge two or more arrays. This method
does not change the existing arrays, but instead returns a new array.
Array.prototype.map
The map() method creates a new array populated with the results of
calling a provided function on every element in the calling array.
.onSnapshot((snapshot) => {
setChats((chats) =>
chats.concat(snapshot.docs.map((doc) => doc.data()))
);
});
Example:
useEffect(() => {
if (user.uid) {
db.collection("Users")
.doc(user.uid)
.collection("ChatRoomIds")
.get()
.then((snapshot) => {
snapshot.docs.map((each) => {
db.collection("ChatRooms")
.where("chatRoomId", "==", each.data().id)
.orderBy("dateLastUpdated", "desc")
.onSnapshot((snapshot) => {
setChats((chats) =>
chats.concat(snapshot.docs.map((doc) => doc.data()))
);
});
});
});
}
}, []);
Update - to resolve duplicates
Still use a functional state update but run the chats array through a filter first to check if there are no new chats with matching chatRoomId properties. If there is a match return false to remove it from the array, otherwise keep it. The filter function returns a new array that you can concatenate to the new chats results.
useEffect(() => {
if (user.uid) {
db.collection("Users")
.doc(user.uid)
.collection("ChatRoomIds")
.get()
.then((snapshot) => {
snapshot.docs.map((each) => {
db.collection("ChatRooms")
.where("chatRoomId", "==", each.data().id)
.orderBy("dateLastUpdated", "desc")
.onSnapshot((snapshot) => {
const newChats = snapshot.docs.map((doc) => doc.data());
setChats((chats) => {
const filtered = chats.filter(chat =>
!newChats.some(newChat =>
newChat.chatRoomId === chat.chatRoomId
)
);
newChats.concat(filtered);
});
});
});
});
}
}, []);

Related

Getting Data using query Params and Firebase with React

I am trying to return a single Booklet from firestore whose ID matches the id from the URL. I am using useParams() to extract the id from the url. When I console.log singleBooklet, it is an empty Array. I am pretty sure the problem is that the component is rendering before the query in the useEffect has time to setSingleBooklet. Any solutions?
const Booklet = () => {
const [singleBooklet, setSingleBooklet] = useState([]);
const params = useParams();
useEffect(() => {
if (params.id) {
const q = query(
collection(db, "Booklet"),
where("bookletId", "==", params.id)
);
const unsubscribe = onSnapshot(q, (querySnapshot) => {
setSingleBooklet(
querySnapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }))
);
});
console.log(singleBooklet);
return unsubscribe;
}
}, []);
return (
<div className="booklet-section row">
<div className="col-md-8 col-sm-10 col-lg-7 col-xl-5 mx-auto">
<Carousel interval={null}>
<Carousel.Item>
<h4 className="text-center">Cover</h4>
<div className="cover">
<img src={ELFlogo} className="cover-logo"></img>
<h1 className="text-secondary">{singleBooklet[0].title}</h1>
<br></br>
<h5>By</h5>
<h3 className="text-secondary"></h3>
</div>
</Carousel.Item>
</Carousel>
</div>
</div>
);
};

how to add data in localstorage in react

I am arslan Chaudhry. currently, I am working on an eCommerce site where I will store data in local storage because I don't have too much backend knowledge. how I can add functionality like delete and update on another folder.
my code is given below.
Books.js
import React from "react";
import "./components/book.css";
import Carousel from "react-multi-carousel";
import "react-multi-carousel/lib/styles.css";
import { FaShoppingCart } from "react-icons/fa";
import { useEffect } from "react";
import { useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { createContext } from "react";
const Books = () => {
let arr=()=>{
let dat=localStorage.getItem("products")
if (dat) {
return JSON.parse(dat)
}
else{
return []
}
}
const [booksData, setbooksData] = useState([]);
const [productData, setproductData] = useState(arr());
let nav = useNavigate();
// slider
const responsive = {
superLargeDesktop: {
breakpoint: { max: 4000, min: 3000 },
items: 5,
},
desktop: {
breakpoint: { max: 3000, min: 1024 },
items: 3,
},
tablet: {
breakpoint: { max: 1024, min: 464 },
items: 2,
},
mobile: {
breakpoint: { max: 464, min: 0 },
items: 1,
},
};
let croser = useRef("");
let loding = useRef("");
const getJason = async () => {
try {
let fea = await fetch(
"https://script.google.com/macros/s/AKfycbxFCG7S-kjncQZwvcMnqq4wXoBAX8ecH1zkY2bLP7EE-YHlnKbiJ3RUuHtWLe6sIK30Kw/exec"
);
let acData = await fea.json();
let itemsData = acData.shop.filter((element) => {
if (element.name) {
return element;
}
});
setbooksData(itemsData);
if (itemsData) {
croser.current.style.filter = "blur(0px)";
loding.current.style.display = "none";
}
} catch (error) {
croser.current.style.filter = "blur(0px)";
loding.current.style.display = "none";
}
};
// get product data from api
useEffect(() => {
getJason();
}, []);
// go to cart button
const goto = () => {
nav("/Cart");
};
// local data
let addNow=(e)=>{
let data=productData.find((element)=>{return element.id === e.id });
let cart;
if (data) {
cart=productData.map((element)=>{
return element.id === e.id ? {...element, quantity:element.quantity+1}:element
})
}
else{
cart=[...productData,{...e, quantity:1}]
}
setproductData(cart);
};
useEffect(() => {
localStorage.setItem("products",JSON.stringify(productData))
}, [productData])
console.log(productData);
return (
<>
<div className="row " style={{ marginTop: "10px" }}>
<div className="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div className="section-headline text-center">
<h2>Books Shop</h2>
</div>
</div>
</div>
<div className="lodingBooks" ref={loding}>
<div class="spinner-border" role="status"></div>
<h4>Please wait....</h4>
</div>
<div ref={croser}>
<div className=" shadow go_to_cart" onClick={goto}>
<i class="bi bi-cart-check text-white"></i>
</div>
<Carousel responsive={responsive} className="container">
{booksData.map((element) => {
return (
<>
<div class="container page-wrapper">
<div class="page-inner">
<div class="row">
<div class="el-wrapper">
<div class="box-up">
<img class="img" src={element.images} alt="" />
<div class="img-info">
<div class="info-inner">
<span class="p-name text-info">
{element.name}
</span>
<span class="p-company ">Author:CHAUDHRY</span>
</div>
<div class="a-size ">
About:This is a complete book on javascript
<span class="size"></span>
</div>
</div>
</div>
<input
type="text"
value={1}
style={{ display: "none" }}
/>
<div class="box-down">
<div class="h-bg">
<div class="h-bg-inner"></div>
</div>
<a class="cart">
<span class="price">{element.price + "$"}</span>
<span
class="add-to-cart btn btn-sm"
style={{ backgroundColor: "#3EC1D5" }}
onClick={()=>{addNow(element)}}
>
<span class="txt">
ADD TO CART <FaShoppingCart />
</span>
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</>
);
})}
</Carousel>
</div>
</>
);
};
export default Books;
and here is my cart file. where i want to perform the action like update and delete.
Cart.js
import React from "react";
import "./components/cart.css";
import { useEffect } from "react";
const Cart = () => {
let data = localStorage.getItem("products");
let javaData = JSON.parse(data);
let removeData = (e) => {
};
useEffect(() => {
localStorage.clear()
}, [])
return (
<>
<div class="container mt-5 mb-5">
<div class="d-flex justify-content-center row">
<div class="col-md-8">
<div class="p-2 shoingTitle">
<h4>Shop Now</h4>
<span class="text-danger">Remove all</span>
</div>
{javaData ? (
javaData.map((item) => {
return (
<>
<div class="d-flex flex-row justify-content-between align-items-center p-2 bg-white mt-4 px-3 rounded">
<div class="mr-1 imageandpara">
<img class="rounded" src={item.images} width="70" />
<span class="font-weight-bold">{item.name}</span>
</div>
<div class="d-flex flex-column align-items-center product-details">
<div class="d-flex flex-row product-desc"></div>
</div>
<div class="d-flex flex-row align-items-center qty">
<i class="minusSign shadow">
<i class="bi bi-dash"></i>
</i>
<span class="text-grey quantityNumber">
{item.quantity}
</span>
<i class="minusSign shadow">
<i class="bi bi-plus"></i>
</i>
</div>
<div>
<span class="text-grey productAmount">{`${
item.quantity * item.price
}$`}</span>
</div>
<div
class="d-flex align-items-center text-dark "
style={{
cursor: "pointer",
fontWeight: "900",
fontSize: "15px",
}}
onClick={() => {
removeData(item);
}}
>
<i class="bi bi-x text-danger"></i>
</div>
</div>
</>
);
})
) : (
<h3 style={{ textAlign: "center" }}>Cart is empety</h3>
)}
<div class="d-flex flex-row align-items-center mt-3 p-2 bg-white rounded">
<input
type="text"
class="form-control gift-card "
placeholder="discount code/gift card"
/>
<button
class="btn btn-sm ml-3 shadow"
type="button"
style={{
outline: "#3EC1D5",
backgroundColor: "#3EC1D5",
color: "white",
}}
>
Apply
</button>
</div>
<div class="totalItems">
Total Items: <strong>12</strong>
</div>
<span class="TotalPrice">
Total price: <strong>12$</strong>
</span>
<div class="d-flex flex-row align-items-center mt-3 p-2 bg-white rounded">
<button
class="btn btn-block btn-sm ml-2 pay-button shadow"
type="button"
style={{ backgroundColor: "#3EC1D5" }}
>
Proceed to Pay
</button>
</div>
</div>
</div>
</div>
</>
);
};
export default Cart;
Try this for add:
let removeData = (e) => {
localStorage.setItem("name of the item") // e.target.name
};
There's alot going on in your site :)
I think it will be responsible to create a context, that will serve the cart to all other components.
Things to note here, (Found some little improvements)
Run the function in the useState hook, don't just leave it there like you did
When using Array.filter you need to return a boolean iorder for it too filter your array properly.
This is the code I brought up hope it helps you out.
CartContext.js file.
import React, { createContext, useContext, useEffect, useState } from "react";
export const CartContext = createContext();
function Cart({ children }) {
const arr = useCallback(() => {
let dat = localStorage.getItem("products");
if (dat) {
return JSON.parse(dat);
} else {
return [];
}
}, []);
const [productData, setproductData] = useState(() => arr());
const getJason = async () => {
try {
let fea = await fetch(
"https://script.google.com/macros/s/AKfycbxFCG7S-kjncQZwvcMnqq4wXoBAX8ecH1zkY2bLP7EE-YHlnKbiJ3RUuHtWLe6sIK30Kw/exec"
);
let acData = await fea.json();
// filter callback function should return a boolean. That is either true or false in order to make it work.
// SO i think this function isn't going to function properly
let itemsData = acData.shop.filter((element) => {
if (element.name) {
return element;
}
});
setbooksData(itemsData);
if (itemsData) {
croser.current.style.filter = "blur(0px)";
loding.current.style.display = "none";
}
} catch (error) {
croser.current.style.filter = "blur(0px)";
loding.current.style.display = "none";
}
};
// get product data from api
useEffect(() => {
getJason();
}, []);
const addProduct = (e) => {
// check if product id available on cart
const findProduct = productData.find((element) => {
return element.id === e.id;
});
// add first quantity if not available
if (!findProduct)
return setproductData([...productData, { ...e, quantity: 1 }]);
// increase quantity by 1
const newCart = productData.map((element) => {
return {
...element,
quantity: element.id === e.id ? element.quantity + 1 : element.quantity,
};
});
setproductData(newCart);
};
const removeProduct = (e) => {
// check if product id available on cart
const findProductQuantity = productData.find((element) => {
return element.id === e.id && element.quantity >= 1;
});
// add first quantity if not available
if (!findProduct)
// Your ui should prevent this
return;
// decrease quantity by 1
const reducedQuantityCart = productData.map((element) => {
return {
...element,
quantity: element.id === e.id ? element.quantity - 1 : element.quantity,
};
});
// filter out all products with quantity less than 1 (quantity : 0)
const newCart = productData.filter((element) => {
return element.quantity >= 1;
});
setproductData(newCart);
};
const deleteProduct = (e) => {
// check if product id available on cart
const findProduct = productData.find((element) => {
return element.id === e.id;
});
// add first quantity if not available
if (!findProduct)
// Your ui should prevent this
return;
const productIndex = productData.findIndex((element) => {
return element.id === e.id;
});
// splice (delete) product from the productData array
const newCart = [productData].splice(productIndex, 1);
setproductData(newCart);
};
const value = {
productData,
addProduct,
removeProduct,
deleteProduct,
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
In here you create a context, create all your function you will use to update your cart, and pass them to your Context provider
// create a hook can you use anywhere in the app
export const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error("Must use useCart in CartContext Child");
}
const { productData, addProduct, removeProduct, deleteProduct } = context;
return { productData, addProduct, removeProduct, deleteProduct };
};
For this you create a custom hook that you can use inside any component provided it is a child of the Cart context. So make sure you wrap it all around your app
// Use Case
const SomeComponent = () => {
const { productData, addProduct, removeProduct } = useCart();
return (
<div>
<p> Number of Products: {productData.length}</p>
<div>
{productData?.map((product) => (
<div>
<p>{product?.name}</p>
<button onClick={() => addProduct(product)}>add</button>
<button onClick={() => removeProduct(product)}>
subtract/reduce
</button>
<button onClick={() => deleteProduct(product)}>delete</button>
</div>
))}
</div>
</div>
);
};
Use case Scenario od how this code will work. Hope you find this helful

useEffect runs Mutiple times and upadting the views

I have A Component (the GetSingleVideoID Component) that needs to be rendered every time the id changes
You can check the code on Github the repo Link
the Issue is that the route to get the single video I have put the views of the video to increment when ever the route hits. and then the use Effect keep incrementing the views when I am already on the Component
The getVideoById Route
// #route GET api/v1/video/:id
// #desc GET single video
// #access Private
router.get('/:id', auth, async (req, res) => {
const { id } = req.params;
try {
const videos = await Video.find().populate('channel', ['image', 'title']);
const video = await Video.findById(id).populate('channel', [
'image',
'title',
'subscribers',
'usersToNotify',
]);
if (!video) {
return res.status(404).json({ msg: 'Video not found!' });
}
video.views += 1;
const catVid = {};
video.categoryVids = [];
videos
.filter(
(vid) =>
vid.category === video.category &&
vid._id.toString() !== video._id.toString()
)
.forEach((v) => {
catVid.video = v._id;
catVid.videoTitle = v.title;
catVid.channelTitle = v.channel.title;
catVid.thumb = v.thumbnail;
catVid.views = v.views;
catVid.createdAt = v.createdAt;
video.categoryVids = [...video.categoryVids, catVid];
});
await video.save();
res.status(200).json(video);
} catch (err) {
if (err.kind == 'ObjectId') {
return res.status(400).json({ msg: 'Video not found!' });
}
console.error(err.message);
res.status(500).send('Server Error');
}
});
When I get rid of the func from the dependencies the Subscribe and Notify doesn't work perfectly anymore (like update when i click) I would have to refresh for the changes to work.
import React, { useState, useEffect, useCallback } from 'react';
import moment from 'moment';
import { useParams, Link, useNavigate } from 'react-router-dom';
import { useVideoGlobalContext } from '../actions/video';
import { useAuthGlobalContext } from '../actions/auth';
import { useChannelGlobalContext } from '../actions/channel';
import { timeSince, intToString } from '../utils/utilities';
import Section from '../components/Section';
const SingleVideo = () => {
const {
singleVideo,
loading,
getVideoByID,
commentVideo,
likeVideo,
deleteComment,
likes,
unlikes,
unlikeVideo,
deleteVideo,
} = useVideoGlobalContext();
const { user, isAuthenticated, userChannel } = useAuthGlobalContext();
const { id } = useParams();
const {
notifyChannel,
msg,
newChannel,
unnotifyChannel,
subscribeChannel,
unsubscribeChannel,
} = useChannelGlobalContext();
const [text, setText] = useState('');
const [subscribed, setSubscribed] = useState(false);
const [sub, setSub] = useState(false);
const [liked, setLiked] = useState(false);
const [like, setLike] = useState(false);
const [disliked, setDisliked] = useState(false);
const [dislike, setDislike] = useState(false);
const [notified, setNotified] = useState(false);
const [notify, setNotify] = useState(false);
const navigate = useNavigate();
useEffect(() => {
if (singleVideo) {
const isSubscribed = singleVideo.channel.subscribers.some(
(cha) => cha.user == user._id
);
setSub(subscribed);
setSubscribed(isSubscribed);
}
}, [singleVideo, subscribeChannel, unsubscribeChannel, newChannel]);
useEffect(() => {
if (singleVideo) {
const isLiked = singleVideo.likes.some((like) => like.user == user._id);
setLike(liked);
setLiked(isLiked);
}
}, [singleVideo, likeVideo, unlikeVideo, newChannel]);
useEffect(() => {
if (singleVideo) {
const isLiked = singleVideo.unlikes.some((like) => like.user == user._id);
setDislike(liked);
setDisliked(isLiked);
}
}, [singleVideo, likeVideo, unlikeVideo, newChannel]);
useEffect(() => {
if (singleVideo) {
const isNotified = singleVideo.channel.usersToNotify.some(
(cha) => cha.user == user._id
);
setNotify(notified);
setNotified(isNotified);
}
}, [singleVideo, notifyChannel, unnotifyChannel, msg]);
// // console.log(data);
useEffect(() => {
getVideoByID(id);
}, [id]);
// useEffect(() => {
// getVideoByID(id);
// }, [getVideoByID, id]);
const onSubscribe = () => {
setSub(!sub);
if (sub && subscribed) {
unsubscribeChannel(singleVideo.channel._id);
// if (msg.msg) {
// alert(msg.msg);
// }
// alert(subMsg.msg);
} else {
subscribeChannel(singleVideo.channel._id);
// if (msg.msg) {
// alert(msg.msg);
// }
// alert(subMsg.msg);
}
};
const onNotification = () => {
setNotify(!notify);
if (notify && notified) {
unnotifyChannel(singleVideo.channel._id);
// if (msg.msg) {
// alert(msg.msg);
// }
// console.log(msg);
} else {
notifyChannel(singleVideo.channel._id);
// if (msg.msg) {
// alert(msg.msg);
// }
// console.log(msg);
}
};
const likeAVideo = () => {
setLike(!liked);
if (like && liked) {
alert('You already like this');
} else {
likeVideo(singleVideo._id);
// if (msg.msg) {
// alert(msg.msg);
// }
// console.log(msg);
}
};
const commentAVideo = (e) => {
e.preventDefault();
commentVideo(singleVideo._id, text);
setText('');
};
const dislikeAVideo = () => {
setDislike(!disliked);
if (dislike && disliked) {
alert('You already unlike this');
} else {
unlikeVideo(singleVideo._id);
// if (msg.msg) {
// alert(msg.msg);
// }
// console.log(msg);
}
};
const onDelete = (vid) => {
deleteVideo(vid);
navigate('/');
};
return (
<Section nameClass='video-page'>
{singleVideo === null ? (
<h1>Loading....</h1>
) : (
<div class='v-container'>
<div class='video-player'>
<div class='vid-container'>
<video
src={`http://localhost:5000/${singleVideo.videoPath}`}
controls
></video>
{isAuthenticated &&
userChannel &&
userChannel._id === singleVideo.channel._id ? (
<div>
<Link to={`/video/edit/${singleVideo._id}`}>
<button>
<i class='fa-solid fa-pen-to-square'></i>
</button>
</Link>
<button onClick={() => onDelete(singleVideo._id)}>
<i class='fa-solid fa-trash-can'></i>
</button>
</div>
) : (
''
)}
</div>
<div class='vid-info'>
<div class='tags mb-2 primary'>
{singleVideo.tags.map((tag) => (
<span>#{tag} </span>
))}
</div>
<h3>
{singleVideo.title}{' '}
<span class='badge-cate'>{singleVideo.category}</span>
</h3>
<div class='views-date'>
<span class='views'>
{intToString(singleVideo.views)} views
</span>
<span class='date'>
{moment(singleVideo.createdAt).format('MMM Do YYYY')}
</span>
</div>
<div class='desc'>{singleVideo.description}</div>
<div class='channel-card'>
<div class='channel-info'>
<img
src={
singleVideo.channel.image
? `http://localhost:5000/${singleVideo.channel.image}`
: '/img/no-image.png'
}
alt=''
/>
<div class='name-subs'>
<h4>{singleVideo.channel.title}</h4>
<span class='subs'>
{singleVideo.channel.subscribers.length} Subscribers
</span>
</div>
</div>
<div class='btns'>
<button onClick={likeAVideo} class='thumbs'>
<i
class={`fa-solid fa-thumbs-up ${liked ? 'primary' : ''}`}
></i>
</button>
<span>{singleVideo.likes.length}</span>
<button onClick={dislikeAVideo} class='thumbs'>
<i
class={`fa-solid fa-thumbs-down ${
disliked ? 'primary' : ''
}`}
></i>
</button>
<span>{singleVideo.unlikes.length}</span>
<button
onClick={onSubscribe}
className={`sub ${subscribed ? 'disabled' : ''}`}
>
Subscribe
</button>
<button onClick={onNotification} class='bell'>
<i
className={`fa-solid fa-bell ${
notified ? 'primary' : ''
}`}
></i>
</button>
</div>
</div>
<div class='comments'>
<h4>{singleVideo.comments.length} Comments</h4>
<form class='form' onSubmit={commentAVideo}>
<div class='form-control'>
<input
type='text'
name='text'
placeholder='Add a comment...'
class='input'
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
<button class='btn'>Add</button>
</form>
<div class='comment-container'>
{singleVideo.comments.length > 0 ? (
singleVideo.comments.map((comment) => (
<div class='comment'>
<img
src={
comment.userImage
? `http://localhost:5000/${comment.userImage}`
: '/img/photo-me.jpeg'
}
alt=''
/>
{isAuthenticated && user._id === comment.user ? (
<button
onClick={() =>
deleteComment(singleVideo._id, comment._id)
}
class='delete'
>
<i class='fa-solid fa-trash-can'></i>
</button>
) : (
''
)}
<div class='info'>
<div>
<h5>{comment.userName}</h5>
<span>
{(timeSince(comment.date) ||
timeSince(comment.date) !== undefined) &&
timeSince(comment.date)}
</span>
</div>
<p>{comment.text}</p>
</div>
</div>
))
) : (
<h3 className='text-center'>No Comments</h3>
)}
</div>
</div>
</div>
</div>
<div class='category-videos'>
<h3>
More Videos in <span class='primary'>{singleVideo.category}</span>
</h3>
<div class='category-vids'>
{singleVideo.categoryVids.length > 0 ? (
singleVideo.categoryVids.map((video) => (
<div class='cat-vid'>
<img
src={
video.thumb
? `http://localhost:5000/${video.thumb}`
: '/img/no-image.png'
}
alt=''
/>
<div class='info'>
<h4>{video.videoTitle}</h4>
<span>
{video.channelTitle} <i class='fa-solid fa-check'></i>{' '}
</span>
<div>
<span class='views'>
{intToString(video.views)} views
</span>
<span class='time'>{timeSince(video.createdAt)}</span>
</div>
</div>
</div>
))
) : (
<h4 className='text-center'>
No Videos for {singleVideo.category}...
</h4>
)}
</div>
</div>
</div>
)}
</Section>
);
};
export default SingleVideo;
on the useEffect I have put the getVideoByID function on the dependencies as you can see on the code above.

Keep track of pagination and go back most recent last page number in react

I am working on an ecommerce, where I am using material UI pagination component for implementing pagination. Here is new requirement arises. I need to add functionality in pagination: if user click on let's say respectively 3,7,11,13 if they click on browser back button they will go back to 11 then 7 then 3 and lastly 1. How do I do that?
I am using react, react router dom.
Here is pagination structure:
FYI, this is url and API structure:
URL and API structure
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import ProductList from "../../components/common/ProductList/ProductList";
import {
GET_PRODUCTS_BY_BRAND,
GET_PRODUCTS_BY_CATEGORY,
GET_PRODUCTS_BY_SUBCATEGORY,
GET_PRODUCTS_BY_VENDOR,
} from "../../requests/HomePageApi";
import { Pagination } from "#material-ui/lab";
import "./ShopPage.scss";
const ShopPage = () => {
const { type, slug, subcategory } = useParams();
const [loading, setLoading] = useState(true);
// const [error, setError] = useState(false);
const [brands, setBrands] = useState([]);
const [colors, setColors] = useState([]);
const [sizes, setSizes] = useState([]);
const [products, setProducts] = useState(null);
const [filteredProducts, setFilteredProducts] = useState(null);
const [page, setPage] = React.useState(0);
const [count, setCount] = React.useState(1);
const [limit, setLimit] = React.useState(60);
const [total, setTotal] = React.useState(60);
const [sideFilter, setSideFilter] = useState(false);
const [vandor, setvandor] = useState({
vendorImg: "",
vendorName: "",
vendorSlug: "",
});
const [filter, setFilter] = useState({
// brands: "",
color: "",
size: "",
price: "",
});
const closeSideFilter = () => {
setSideFilter(false);
};
const getProducts = async (slug, qParams) => {
try {
let res;
if (type === "category") {
subcategory
? (res = await GET_PRODUCTS_BY_SUBCATEGORY(
slug,
subcategory,
qParams
))
: (res = await GET_PRODUCTS_BY_CATEGORY(slug, qParams));
}
if (type === "brand") res = await GET_PRODUCTS_BY_BRAND(slug, qParams);
if (type === "store") res = await GET_PRODUCTS_BY_VENDOR(slug, qParams);
if (res) setLoading(false);
if (res && res.products && res.products.length > 0) {
setProducts(res.products);
setFilteredProducts(res.products);
setTotal(res.total);
setCount(Math.ceil(res.total / limit));
if (type === "brand") {
setvandor({
vendorImg: `/assets/images/brand/${res.products[0].brand_logo}`,
vendorName: res.products[0].brand_name,
vendorSlug: res.products[0].brand_slug,
});
} else if (type === "store") {
setvandor({
vendorImg: `/assets/images/brand/${res.products[0].brand_logo}`,
vendorName: res.products[0].shop_name,
vendorSlug: res.products[0].vendorSlug,
});
}
if (res.colors) {
const uniqueColors = [...new Set(res.colors)];
setColors(uniqueColors);
}
if (res.sizes) {
const uniqueSizes = [...new Set(res.sizes)];
setSizes(uniqueSizes);
}
// if (res.brands) setBrands(res.brands);
}
} catch (error) {
console.log(error);
}
};
// console.log({ filteredProducts, filter, page, count, limit, total });
React.useMemo(() => {
let qParams = {
page: page,
limit: limit,
size: filter.size,
color: filter.color,
// brands: filter.brands,
price: filter.price.length ? `${filter.price[0]},${filter.price[1]}` : "",
};
if (slug) {
getProducts(slug, qParams);
}
}, [slug, page, limit, filter, count]);
React.useEffect(() => {
setPage(0);
}, [filter]);
const changeLimit = (limit) => {
setPage(0);
setLimit(limit);
};
const handleChange = (event, value) => {
// window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
setPage(value - 1);
};
const slugTitle = (slug) => slug.split("-").join(" ");
return (
<FadeTransition>
{/* {loading && (
<div className="section-big-py-space ratio_asos py-5">
<div className="custom-container">
<Skeleton type="ShopPage" />
</div>
</div>
)} */}
{!loading && products === null && (
<div className="section-big-py-space ratio_asos py-5">
<div className="custom-container">
<h3 style={{ color: "#32375A", textAlign: "center" }}>
Sorry, No Product Found!
</h3>
</div>
</div>
)}
{products && (
<div className="title-slug-section">
<h2 class="title-slug">{slug && slugTitle(slug)}</h2>
</div>
)}
{products && (
<section className="section-big-py-space ratio_asos">
{/* {type !== "category" && (
<div className="merchant-page-header">
<div className="custom-container">
<div
className="shadow-sm bg-white rounded p-3 mb-5 d-flex align-items-center w-100"
style={{ minHeight: "132px" }}
>
<div className="row align-items-center w-100">
<div className="col-lg-6">
<div className="row align-items-center">
{vandor && vandor.vendorImg && (
<div className="col-auto">
<Image
src={vandor.vendorImg}
alt={vandor.vendorName}
className="img-fluid merchant-img"
/>
</div>
)}
<div className="col-auto mt-lg-0 mt-2">
<h3 className="mb-0"> {vandor.vendorName} </h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)} */}
<div className="collection-wrapper">
<div className="custom-container">
<div className="row">
<div className="col-sm-3 collection-filter category-page-side">
{/* <SidebarFilter
type={type}
brands={brands}
colors={colors}
sizes={sizes}
onChange={(data) => setFilter(data)}
/> */}
<InnerCategory />
{products && (
<RowSlider title="New Products" products={products} />
)}
</div>
<div className="collection-content col-lg-9">
<div className="page-main-content">
<div className="collection-product-wrapper">
<div className="row">
<div className="col-xl-12">
{/* <Button
variant='contained'
className='bg-dark text-light d-lg-none mb-3 mt-2 w-100'
onClick={() => setSideFilter(true)}
>
<span className='filter-btn '>
<i
className='fa fa-filter'
aria-hidden='true'
></i>
Filter
</span>
</Button> */}
</div>
</div>
<MainFilter
type={type}
// brands={brands}
colors={colors}
sizes={sizes}
page={page}
limit={limit}
onCountChange={(c) => changeLimit(c)}
onChange={(data) => setFilter(data)}
/>
{/* <TopFilter
onCountChange={(x) => changeLimit(x)}
total={total}
page={page}
limit={limit}
setSideFilter={setSideFilter}
/> */}
{filteredProducts && (
<ProductList products={filteredProducts} />
)}
{count > 1 && (
<div className="d-flex justify-content-center mt-4">
<Pagination
count={count}
page={page + 1}
onChange={handleChange}
shape="rounded"
/>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
)}
{!loading && products?.length === 0 && (
<div className="merchant-page-header">
<div className="custom-container pt-5">
<div
className="shadow-sm bg-white rounded p-3 mb-5 d-flex align-items-center justify-content-center w-100"
style={{ minHeight: "132px" }}
>
<h3 className="mb-0">No Products found!</h3>
</div>
</div>
</div>
)}
<Drawer
open={sideFilter}
className="add-to-cart"
onClose={() => setSideFilter(false)}
transitionDuration={400}
style={{ paddingLeft: "15px" }}
>
<SidebarFilter
onClose={closeSideFilter}
type={type}
// brands={brands}
colors={colors}
sizes={sizes}
onChange={(data) => setFilter(data)}
/>
</Drawer>
</FadeTransition>
);
};
export default ShopPage;
Usually you would like to have URL to represent selected page, so you could refresh the page and still be on the same page. Or share the exact page via copy-paste of URL. Especially on e-commerce sites. So I would recommend to sync selected page with URL.
While it's so common scenario, i have couple hooks for that. First of all - URL hook, which should work with your reactjs app setup.
https://www.npmjs.com/package/hook-use-url
and then couple more hooks to not worry about pagination details inside component:
usePage.js:
import useUrl from "hook-use-url";
export default function usePage() {
const url = useUrl();
const page = url.get({ variable: "page" })
? parseInt(url.get({ variable: "page" }), 10)
: 1;
const setPage = (value) => {
url.multipleActions({
setPairs: [{ variable: "page", value: value }],
});
};
return [page, setPage];
}
and usePerPage.js:
import useUrl from "hook-use-url";
export default function usePerPage() {
const url = useUrl();
const perPage = url.get({ variable: "per-page" })
? parseInt(url.get({ variable: "per-page" }), 10)
: 25;
const setPerPage = (value) => {
url.multipleActions({
setPairs: [
{ variable: "page", value: 1 },
{ variable: "per-page", value },
],
});
};
return [perPage, setPerPage];
}
Inside components you can use these like so:
(Take a note that which page is 1st depends on your backend API, in my case 1st page is always 1 and not 0, but mui.com component starts from 0 that's why there is -1 and +1).
function MyComp(){
const [page, setPage] = usePage();
const [perPage, setPerPage] = usePerPage();
// ....
return (
<TablePagination
count={totalRows}
page={page - 1}
onPageChange={(e, newPage) => {
setPage(newPage + 1);
}}
rowsPerPage={perPage}
onRowsPerPageChange={(e) => {
setPerPage(e.target.value);
}}
/>
)
}

How to push an array with a hook on react js?

I'm trying to render an array of value . This is basicaly a to do list and i want to render different value with the button "en cours"
My .map doesn't seems to work and my array just record one value. someone can help me ?
There is also an other problem, when i write on my input my letters just disappeared directly ??
function Task() {
const [task, setTask] = useState("");
const [encours, setEncours] = useState("");
let toDo = [];
const handleInputTask = (e) => {
setTask(e.target.value);
setEncours("en cours");
};
function AddTask() {
toDo.push(task);
console.log(toDo);
}
const switchEnCours = () => {
setEncours("terminé");
};
const deleteTask = () => {
setEncours("supprimée");
};
function RenderMesTasks() {
return (
<div>
<input onChange={handleInputTask}></input>
<button onClick={AddTask}>Valider</button>
<div className="DivColonne">
<div className="Colonne">
<h1>Tâche à faire</h1>
{toDo !== "" && encours === "en cours" ? (
toDo.map((insertTask) => {
<div>
<p>{insertTask}</p>
<button onClick={switchEnCours}>{encours}</button>
</div>;
})
) : (
<div></div>
)}
</div>
<div className="Colonne">
<h1>Tâche en cours</h1>
{encours === "terminé" ? (
<div>
{toDo.map((insert) => {
return (
<div>
<p>{insert}</p>
<button onClick={deleteTask}>{encours}</button>
</div>
);
})}
</div>
) : (
<div></div>
)}
</div>
<div>
<h1>Tâches terminées</h1>
{encours === "supprimée" ? (
<div>
<p>{toDo}</p>
</div>
) : (
<div></div>
)}
</div>
</div>
</div>
);
}
return (
<div>
<h2> Ecrivez une nouvelle tâche</h2>
<div>
<RenderMesTasks />
</div>
</div>
);
}
export default Task;
todos is state:
const [todos,setTodos] = useState([]);
const addToDo = (todo) => setTodos([...todos,todo])
Add this state variable
const [todoList, setTodoList] = useState([]);
and then in your AddTask(), add this
function AddTask() {
setTodoList([...todoList, task]);
}

Resources