React Firebase deleting the wrong document id - javascript

I've been trying to make a delete operation on a firebase database using Reactjs. I've got a bug with my function grabbing the wrong id from firebase.
I have a button that calls a handleOpen function which opens a Modal.
Modal operations:
// Grabs the right id
const [open, setOpen] = useState(false);
const handleOpen = (id) => {
console.log(id);
setOpen(true);
};
const handleClose = () => setOpen(false);
I've got a button that calls a handleDelete function which grabs the document id reference and deletes the document reference.
handleDelete function:
const handleDelete = (id) => {
const docRef = projectFirestore.collection("News").doc(id);
docRef.delete();
console.log("Deleted post data from id: " + id);
handleClose();
};
The Problem
From what I've been watching the handleDelete function grabs the last id from the mapped array of posts, it doesn't pass the id of the current document to the modal.
The problem only happens when I pass the function inside the modal. When I pass the function outside of the modal it works just fine.
The Objective
Grabbing document id, passing it to the modal and deleting the respective document.
Here's the full code:
import React, { useState } from "react";
import { projectFirestore } from "../../../../firebase/config";
import { useCollectionData } from "react-firebase-hooks/firestore";
import Layout from "../../../../hoc/Layout/Layout";
import { Link } from "react-router-dom";
import { Button, Box, Modal } from "#mui/material";
const DeletePost = () => {
const docRef = projectFirestore.collection("News");
const query = docRef.orderBy("createdAt", "desc");
const [posts] = useCollectionData(query, { idField: "id" });
// Modal operations
const [open, setOpen] = useState(false);
const handleOpen = (id) => {
setOpen(true);
};
const handleClose = () => setOpen(false);
const handleDelete = (id) => {
const docRef = projectFirestore.collection("News").doc(id);
docRef.delete();
console.log("Deleted post data from id: " + id);
handleClose();
};
return (
<Layout>
<ul>
{posts &&
posts.map((post) => {
const data = post.createdAt.toDate();
const day = data.getUTCDate();
const month = data.getUTCMonth();
const year = data.getUTCFullYear();
return (
<li key={post.id}>
<div>
<h3>{post.id}</h3>
<img
src={post.url}
alt={post.title}
/>
<p>
<b>
{day}/{month}/{year}
</b>{" "}
{post.description}
</p>
</div>
<div>
<Link to="/edit-post">
<Button>
Edit Post
</Button>
</Link>
<Button onClick={() => handleOpen()}>
Delete Post
</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="Delete"
aria-describedby="Delete Post"
>
<Box>
<div>
<h4>
Are you sure you want to delete {post.title}?
</h4>
</div>
<div>
<Button
onClick={() => {
debugger;
handleDelete(post.id);
}}
>
Yes
</Button>
<Button onClick={handleClose}>
No
</Button>
</div>
</Box>
</Modal>
</div>
</li>
);
})}
</ul>
</Layout>
);
};
export default DeletePost;

You could define a state variable that keeps tracks of the currently editing ID:
const [selectedId, setSelectedId] = useState(-1);
Then edit your handleOpen and handleClose functions:
const handleOpen = (id) => {
setSelectedId(id);
setOpen(true);
};
const handleClose = () => {
setSelectedId(-1);
setOpen(false);
};
In the handleDelete function, if an ID is selected, delete that one:
const handleDelete = (id) => {
const docRef = projectFirestore.collection('News').doc(selectedId !== -1 ? selectedId : id);
docRef.delete();
console.log('Deleted post data from id: ' + id);
handleClose();
};
Finally, you will need to update the handleOpen method in the JSX by adding the id parameter:
<Button onClick={() => handleOpen(post.id)}>
Delete Post
</Button>

Related

How can I change the state of individual elements in a map function?

I want the content to display when the tab is clicked. The issue that I'm having is that once the tab is clicked, all the tabs open... and likewise close when clicked again. I've been trying for hours to figure out how to fix this. I thought I had an answer by having a state that I could set the index to and then write a condition for the tab to open when the index of the state is the same but I noticed that after clicking on another tab, the other one closes. I would appreciate it so much if someone could help me open an individual tab when it's clicked and always stay open until clicked again, meaning, I could have multiple tabs open at once.
Here's a demo:
https://codesandbox.io/s/orrigenda-react-question-5oxg47
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import LeaguesStyle from '../components/styles/LeaguesStyle.css';
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false)
const [isOpen, setOpen] = useState(false);
const getTeams = async () => {
try {
const res = await axios.get('https://api-football-standings.azharimm.site/leagues');
setTeams(res.data.data)
setLoading(true);
console.log(res.data)
} catch (err) {
alert(err.message)
}
}
useEffect(() => {
getTeams();
}, []);
return (
<div className="leagues">
{loading &&
teamz.map(item => (
<div className='teamCard' key={item.id}>
<div onClick={() => setOpen(!isOpen)} className="teamDiv">
<img src={item.logos.dark} className='teamLogo' />
<h1>{item.name}</h1>
</div>
{isOpen && <div className='card-content-active'>{item.abbr}</div>}
</div>
))}
</div>
);
}
You need to track the individual truthy values per item.id. This can be easily done by using an object to keep track of all the previous states via the spread operator. Once an initial state is set per tab, then it's just a matter of toggling that individual state between true and false. You delineate between tabs by dynamically assigning the id to the truthy value ([id]: !isOpen[id]). Here is the code in totality:
import React, { useEffect, useState } from "react";
import axios from "axios";
import LeaguesStyle from "./LeaguesStyle.css";
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false);
const [isOpen, setOpen] = useState({});
const getTeams = async () => {
try {
const res = await axios.get(
"https://api-football-standings.azharimm.site/leagues"
);
setTeams(res.data.data);
setLoading(true);
console.log(res.data);
} catch (err) {
alert(err.message);
}
};
useEffect(() => {
getTeams();
}, []);
const handleOpen = (id) => {
setOpen((prevTruthys) => ({ ...prevTruthys, [id]: !isOpen[id] }));
};
console.log(isOpen);
return (
<div className="leagues">
{loading &&
teamz.map((item) => (
<div className="teamCard" key={item.id}>
<div onClick={() => handleOpen(item.id)} className="teamDiv">
<img src={item.logos.dark} className="teamLogo" alt="logo" />
<h1>{item.name}</h1>
</div>
{isOpen[item.id] === true && (
<div className="card-content-active">{item.abbr}</div>
)}
</div>
))}
</div>
);
};
export default Leagues;
Here is the code sandbox: https://codesandbox.io/s/orrigenda-react-question-forked-42lbfo?file=/src/App.js
The solution is to store all clicked tabs in a list using the item ID, when the tab is open and you clicked again the ID is removed from the list
here is the code with the solution:
I created a function to update the state. setOpenById(tabId) and a function for checking if the tab is open isTabOpen(tabId)
the onClick now uses that function onClick={() => setOpenById(item.id)}
import React, { useEffect, useState } from "react";
import axios from "axios";
import LeaguesStyle from "./LeaguesStyle.css";
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false);
const [openTab, setOpenTab] = useState([])
const getTeams = async () => {
try {
const res = await axios.get(
"https://api-football-standings.azharimm.site/leagues"
);
setTeams(res.data.data);
setLoading(true);
//console.log(res.data);
} catch (err) {
alert(err.message);
}
};
useEffect(() => {
getTeams();
}, []);
const setOpenById = (tabId) => {
if(!isTabOpen(tabId)){
setOpenTab([...openTab, tabId])
} else{
var array = [...openTab] // make a separate copy of the array
var index = array.indexOf(tabId)
if (index !== -1) {
array.splice(index, 1)
setOpenTab(array)
}
}
}
const isTabOpen = (tabId) => {
return openTab.indexOf(tabId) !== -1
}
return (
<div className="leagues">
{loading &&
teamz.map((item) => (
<div className="teamCard" key={item.id}>
<div onClick={() => setOpenById(item.id)} className="teamDiv">
<img src={item.logos.dark} className="teamLogo" alt="logo" />
<h1>{item.name}</h1>
</div>
{isTabOpen(item.id) && <div className="card-content-active">{item.abbr}</div>}
</div>
))}
</div>
);
};
export default Leagues;

Call Function out of React Component

I want to update a shopping cart when I click on a Product. But I don't know, how to call the function in another component.
This is, where the function is written and the state of the cart is hold.
export const Cart = () => {
const [userId, setUserId] = useState(7);
const [cart, setCart] = useState([]);
const [outfitName, setOutfitName] = useState("");
const sendOutfit = () => {
axios.post(`{url}/postOutfit`, {
userId,
outfitName,
cart,
});
};
function addToCart(id) {
cart.push(id);
setCart(cart);
}
...
}
Here I want to call the addToCart function.
import { Cart } from "../../sites/Cart/Cart";
...
<div className="product-name">
<button
className="button is-small is-outlined is-primary"
onClick={() =>
Cart.addToCart(product.id) & changeButtonText
}
>
{buttonText}
</button>
</div>
When I try to execute this, I get the following error message:
Do you have any suggestion for me?
Thank you very much!
You can not do this like that. Below I wrote simple example and here is nice article I suggest to read it first: Components and Props
const AddToCartButton = ({ setCart }) => {
return (
<button
onClick={() => {
setCart("item");
}}
></button>
);
};
const Cart = () => {
const [cart, setCart] = React.useState([]);
return <AddToCartButton setCart={setCart} />;
};
Methods in React return JSX values ​​and in this way it is not correct to export a method, if you want to export the method of addToCart to a another component you need to send this method as propeties or using state management for example as follows:
export const Cart = () => {
const [userId, setUserId] = useState(7);
const [cart, setCart] = useState([]);
const [outfitName, setOutfitName] = useState("");
const sendOutfit = () => {
axios.post(`{url}/postOutfit`, {
userId,
outfitName,
cart,
});
};
function addToCart(id) {
cart.push(id);
setCart(cart);
}
return <AnotherComponent addCartFunc={addToCart} />
}
Then you can use this method in the host component as follows:
export const Cart = ({addCartFunc}) => {
return (
<div className="product-name">
<button
className="button is-small is-outlined is-primary"
onClick={() =>
addCartFunc(product.id) & changeButtonText
}
>
{buttonText}
</button>
</div>
)
}

React OnClick iteration

I want to do an onClick counter but I have a problem with the counter iterating correctly. In the app there are 3 "products" and after clicking "Add To Cart" button the state of the object is updated but all of the products are generated separately. I think that is cousing the problem where the counter is different for each of the products or everything will work correctly if I lift the state up, but the console.log is just freshly generated for all of the products. I'm not really sure so I need help with that.
Here is some code in the order from the parent to the last child:
import { useEffect, useState } from "react";
import ProductList from "./ProductList";
const Products = () => {
const [products, setProducts] = useState (null);
useEffect (() => {
fetch('http://localhost:8000/products')
.then(res => {
return res.json();
})
.then(data => {
setProducts(data);
})
}, []);
return (
<div className="ProductList">
{products && <ProductList products={products}/>}
</div>
);
}
export default Products;
import Card from "./Card";
const ProductList = (props) => {
const products = props.products;
return (
<div className="ProductList" >
{products.map((product) => (
<Card product={product} key={product.id} />))}
</div>
);
}
export default ProductList;
import { useState } from "react";
const Card= ({ product }) => {
const [showDescription, setShowDescription] = useState(false);
const [CartCounter, setCartCounter ] = useState(0);
console.log(CartCounter);
return (
<div className="Product-Preview" >
<div className="backdrop" style={{ backgroundImage: `url(${product.image})` }}></div>
<h2>{product.title}</h2>
<div>{product.price}</div>
<button className="ShowDescription" onClick={() => setShowDescription(!showDescription)}>Details</button>
<button className="AddToCart" onClick={() => setCartCounter(CartCounter + 1)}>Add To Cart </button>
{showDescription && <p>{product.description}</p>}
<br />
</div>
);
};
export default Card;
Ok, you want to keep track of an aggregated value. I'll list code in some high level.
const ProductList = () => {
const [count, setCount] = useState(0)
const addOrRemove = n => { setCount(v => v + n) }
return products.map(p => <Card addOrRemove={addOrRemove} />)
}
const Card = ({ addOrRemove }) => {
// optional if you want to track card count
// const [count, setCount] = useState(0)
return (
<>
<button onClick={() => { addOrRemove(1) }>Add</button>
<button onClick={() => { addOrRemove(-1) }>Remove</button>
</>
)
}
Essentially either you track the local count or not, you need to let the parent to decide what is the final count, otherwise there'll be some out of sync issue between the child and parent.

How do I only re-render the 'Like' button in React JS?

const SiteDetails = ({site, user}) => {
const dispatch = useDispatch()
const updateSite = async (siteId, siteObject) => {
try {
const updatedSite = await siteService.update(siteId, siteObject)
dispatch(toggleStatus(siteId, updatedSite))
// dispatch(setNotification(`One like added to ${updatedSite.title}`))
} catch (exception) {
console.log(exception)
dispatch(setNotification("Could not update site"))
}
}
return (
<div className='site-details'>
<Link href={site.url}>{site.url}</Link>
<h2>
<LikeButton user={user} site={site} updateSite={updateSite} />
<VisitButton user={user} site={site} updateSite={updateSite} />
</h2>
<p>{site.description}</p>
<img src={site.imageUrl} alt={"Image could not be loaded"} />
</div>
)
}
const LikeButton = ({user, site, updateSite}) => {
const dispatch = useDispatch()
const likedList = site?.userLiked.find(n => n?.username === user.username)
useEffect(() => {
if (!likedList) {
const newUser = { username: user.username, liked: false }
const updatedArray = site.userLiked.concat(newUser)
const updatedSite = {...site, userLiked: updatedArray}
updateSite(site.id, updatedSite)
}
},[])
const liked = likedList?.liked
const handleLike = async () => {
const indexCurr = site.userLiked.indexOf(likedList)
//updatedSite is the parent site. This will have its liked status toggled
//actually updatedUserLiked can simply use username: user.username and liked: true
const updatedUserLiked = { username: likedList?.username, liked: !likedList.liked}
site.userLiked[indexCurr] = updatedUserLiked
const updatedSite = {...site, userLiked: site.userLiked}
updateSite(site.id, updatedSite)
//childSite is the spawned from the parent,
//it will contain a parent, which is the updatedSite
const childSite = {...site, parent: updatedSite, opcode: 100}
const newSite = await siteService.create(childSite)
// dispatch(createSite(newSite))
dispatch(initializeUsers())
dispatch(initSites())
}
return (
<Button
size='small' variant='contained'
color={liked ? 'secondary' : 'primary'}
onClick={!liked ? handleLike : null} className='site-like'>{liked ? 'Already Liked' : "like"}
</Button>
)
}
Expected Behaviour
Actual Behaviour
Hi everyone, my goal is to make it such that when the 'Like' button is clicked, the appearance of the button will change but the rest of the page does not refresh. However, if you look at the actual behaviour, after the button is clicked, the page is re-rendered and is restored to its default state. I have made my own component which upon clicking will display further details below.
I have tried looking at React.memo and useRef, none of which worked for me. Help on this would be very much appreciated. Thanks!
Have tried using the 'useState' hook?
import {useState} from 'react'
function LikeButton() {
//set default value to false
const [liked, setLiked] = useState(false);
const handleClick = () => {setLiked(!liked)}
return(
<Button color={liked? 'secondary' : 'primary'} onClick={() => handleClick()}
/>
);
}
export default LikeButton;
this should only re-render the button
check this out as well:
ReactJS - Does render get called any time "setState" is called?

MUI - How to open Dialog imperatively/programmatically

Normally this is how you use MUI Dialog. The code below is taken from the docs:
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open Dialog
</Button>
<Dialog open={open} onClose={handleClose}>
{...}
</Dialog>
</div>
);
}
But I want it to create the Dialog imperatively, sort of like fire and forget. I do not want to embed the Dialog component in other components whenever I need to create them. Ideally I'd want to call it like this
createDialog(<>
<h1>My Dialog</h1>
<span>My dialog content</span>
<button onClick={() => closeDialog()}>Close</button>
</>)
So my component definition'd look like this
const createDialog = () => {
// ???
}
const closeDialog = () => {
// ???
}
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => setOpen(true);
const handleClose = () => {
createDialog(<>
<h1>My Dialog</h1>
<span>My dialog content</span>
<button onClick={() => closeDialog()}>Close</button>
</>)
};
return (
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open Dialog
</Button>
);
}
You can reuse dialogs using React's Provider pattern. The official React document has explained in good detail so I won't cover it again here.
First create a custom Provider component in this case I'll call DialogProvider. This component will manage a list of Dialogs in local state.
const DialogContext = React.createContext();
export default function DialogProvider({ children }) {
const [dialogs, setDialogs] = React.useState([]);
return (
<DialogContext.Provider {...}>
{children}
</DialogContext.Provider>
);
}
As you can see, we have an array of dialogs here, it contains the dialog props that will be mapped to the actually <Dialog /> component when rendering.
export default function DialogProvider({ children }) {
const [dialogs, setDialogs] = React.useState([]);
return (
<DialogContext.Provider {...}>
{children}
{dialogs.map((dialog, i) => {
return <DialogContainer key={i} {...dialog} />;
})}
</DialogContext.Provider>
);
}
The <DialogContainer/> is the parent component of the <Dialog/>. Put anything that you want to be reusable in there. Here is a minimum example to get you started.
function DialogContainer(props: DialogContainerProps) {
const { children, open, onClose, onKill } = props;
return (
<Dialog open={open} onClose={onClose} onExited={onKill}>
{children}
</Dialog>
);
}
We can create and remove the dialog using setState as normal.
const [dialogs, setDialogs] = React.useState([]);
const createDialog = (option) => {
const dialog = { ...option, open: true };
setDialogs((dialogs) => [...dialogs, dialog]);
};
const closeDialog = () => {
setDialogs((dialogs) => {
const latestDialog = dialogs.pop();
if (!latestDialog) return dialogs;
if (latestDialog.onClose) latestDialog.onClose();
return [...dialogs].concat({ ...latestDialog, open: false });
});
};
But how do we call them in other components when we defined them here? Well, remember we're using Provider component here, which means we can pass the context data down so other components can reference, in this case we want to pass the createDialog and closeDialog down.
const [dialogs, setDialogs] = React.useState([]);
const createDialog = (option) => {/*...*/};
const closeDialog = () => {/*...*/};
const contextValue = React.useRef([createDialog, closeDialog]);
return (
<DialogContext.Provider value={contextValue.current}>
{children}
{dialogs.map((dialog, i) => ...)}
</DialogContext.Provider>
);
We're almost done here, now we need to add the DialogProvider to the component tree.
export default function App() {
return (
<DialogProvider>
<App {...} />
</DialogProvider>
);
}
But before we can use them, we should create a hook to easily access the context from the parent. So in your DialogProvider.jsx
export const useDialog = () => React.useContext(DialogContext);
Now we can use it like this.
import { useDialog } from "./DialogProvider";
export default function Content() {
const [openDialog, closeDialog] = useDialog();
const onOpenDialog = () => {
openDialog({
children: (
<>
<DialogTitle>This dialog is opened imperatively</DialogTitle>
<DialogContent>Some content</DialogContent>
<DialogActions>
<Button color="primary" onClick={closeDialog}>Close</Button>
</DialogActions>
</>
)
});
};
return (
<Button variant="contained" onClick={onOpenDialog}>
Show dialog
</Button>
);
}
Live Demo
You can play around in the live demo here

Categories

Resources