Firebase with React - unable to delete items from the database - reactjs

Below is my App.js component-
import { useEffect, useState } from 'react'
import Cart from './Cart'
import Navbar from './Navbar'
import firebase from './firebase'
function App() {
const [products, setProducts] = useState([])
useEffect(()=>{
const ref = firebase.firestore().collection('products');
ref.onSnapshot((snapshot) => {
const items = [];
snapshot.forEach((doc) =>{
const data = doc.data();
items.push(data)
data['id'] = doc.id
})
setProducts(items);
})
},[]);
const handleIncreaseQuantity = (product) =>{
const index = products.indexOf(product)
products[index].qty += 1;
setProducts([...products])
}
const handleDecreaseQuantity = (product) =>{
const index = products.indexOf(product)
if(products[index].qty > 0){
products[index].qty -= 1;
setProducts([...products])
}
}
const handleDeleteQuantity = (product) =>{
// const productId = product.id
// const newProducts = products.filter((item) => (item.id !== productId))
// setProducts([...newProducts])
// console.log(newProducts)
// console.log(product.prototype)
firebase.firestore().collection('products').doc(product.id).remove()
console.log(`product id: ${product.id}`)
}
const getCartCount = () =>{
var count = 0;
products.forEach((item) => (
count += item.qty
))
return count;
}
const getCartTotal = () =>{
var count = 0;
products.map((product) =>(
count += product.qty*product.price
))
return count;
}
return (
<div className="App">
<Navbar count = {getCartCount()}/>
<Cart
products = {products}
onIncreaseQty = {handleIncreaseQuantity}
onDecreaseQty = {handleDecreaseQuantity}
onDeleteQty = {handleDeleteQuantity}
/>
<div className="cart-total">
TOTAL: {getCartTotal()}
</div>
</div>
);
}
export default App;
I want my app to function in a way that whenever I click the delete icon(see the image at the last), the item is deleted from the database. I just don't know how to do it. I am using .remove() (see handleDeleteQuantity() function) but it's throwing an error that .remove is not a function. Or maybe I don't know exactly what code goes in handleDeleteQuantity() to remove the item.

The .remove() method is used to delete data from realtime database. Firestore's documentation says, "To delete a document, use the delete() method." Also try awaiting that method in an async function or just handle the promise using .then and .catch.
const handleDeleteQuantity = (product) => {
firebase.firestore().collection('products').doc(product.id).delete().then(() => {
console.log(`product id: ${product.id}`)
}).catch((e) => console.log(e))
}
Deleting a document does not delete its subcollections!

Related

React.useEffect running after .map used in return statement

I am trying to create a react app that takes data from backend and displays it, and each data row from backend can have user defined number of inputs linked to it.
The code looks like this
import React,{useState} from 'react'
export default function Example(){
const [dataFromBackend,setDataFromBackend] = useState([])
const [val,setVal] = useState([[[]]])
const [usrInput,setUsrInput] = useState([[[]]])
React.useEffect(()=>{
const getData = async () =>{
fetch("url")
.then(response => response.json())
.then(setDatafromBackend)
}
getData()
},[])
React.useEffect(()=>{
const DataStructure = async () =>{
let temp = [[[]]], temp2 = [[[]]]
dataFromBackend.map((data,indx)=>{
if(indx!=0) //skipping indx == 0 temp[0] already there
{temp = [...temp,[[]]]
temp2 = [...temp2,[[]]]
}
})
setVal(temp)
setUsrInput(temp2)
}
DataStructure()
},[dataFromBackend])
const handleAdd = (indx) =>{
let tempVal = [...val]
tempVal[indx] = [...tempVal[indx],[]]
setVal(tempVal)
let tempUsrInput = [...usrInput]
tempUsrInput[indx] = [...tempUsrInput[indx],[]]
setUsrInput(tempUsrInput)
}
const handleUsrInput= (indx,indx2) =>{
let temp = [...usrInput]
temp[indx][indx2] = e.target.value;
setUsrInput(temp)
}
const handleDelete = (indx,indx2) =>{
const deleteVal = [...val]
const deleteUsrInput = [...usrInput]
deleteVal[indx].splice(indx2,1)
deleteUsrInput[indx].splice(indx2,1)
setVal(deleteVal)
setUsrInput(deleteUsrInput)
}
return(
{dataFromBackend.map((data,indx)=>{
return(
<div key ={indx}>
<h1>{data.value}</h1>
<button type="button" onClick = {e=>handleAdd(indx)}> Add </button>
{val[indx].map((data2,indx2)=>{
return(
<div key ={indx2}>
<input placeholder = "user input" value = {val[indx][indx2]} onChange = {e=>handleUsrInput(e,indx,indx2)}
<button type="button" onClick = {e=>handleDelete(indx,indx2)}>x</button>
</div>
)
})
}
</div>
)
})
}
)
}
so when I run this
error : val[indx] is not iterable or cannot read properties of undefined (reading : map)
now when I console.log val and data, val is not changed while data is already changed which causes val.length to be unchanged(=1) and not equal to the number of rows in dataFromBackend.
I do not know what I am doing wrong any help would be appreciated.

Array is shuffling in React JS

I'm trying to display an array with 151 pokemons sorted by their pokedex positions (ex: 1 - bulbasaur, 2- ivysaur...), but every time I reload the page it brings me a different array, with the positions shuffled.
App.js:
import './App.css';
import { useEffect, useState } from 'react';
import Pokemon from './components/Pokemon';
const App = () => {
const [pokemonData, setPokemonData] = useState([]);
const fetchPokemons = () => {
for (let index = 1; index < 152; index += 1) {
new Array(151).fill(
fetch(`https://pokeapi.co/api/v2/pokemon/${index}`)
.then((data) => data.json())
.then((result) =>
setPokemonData((prevState) => [...prevState, result])
)
);
}
};
useEffect(() => {
fetchPokemons();
}, []);
return (
<>
{pokemonData && <Pokemon data={pokemonData} />}
</>
);
};
export default App;
Pokemon.js:
const Pokemon = ({ data }) => {
return (
<section>
{data.map((pokemon) => (
<div key={pokemon.name}>
<img src={pokemon.sprites.front_default} alt={pokemon.name} />
<span>{pokemon.name}</span>
<span>
{pokemon.types.length >= 2
? `${pokemon.types[0].type.name} ${pokemon.types[1].type.name}`
: pokemon.types[0].type.name}
</span>
</div>
))}
</section>
);
};
export default Pokemon;
If you want to guarantee the order, you'll need something like
const fetchPokemons = async () => {
const promises = [];
for (let index = 1; index < 152; index += 1) {
promises.push(fetch(`https://pokeapi.co/api/v2/pokemon/${index}`).then((data) => data.json()));
}
const results = await Promise.all(promises);
setPokemonData(results);
};
This will take a while as it loads all of the pokémon before showing any of them - if you don't want that, then there really are two options: rework things to do each request sequentially, or alternately switch to an array where some of the slots may be null while things are still being loaded (which will require changing your rendering code some too).
You are making 153 calls to api, which is not great I would highly recommend that you change into single api call to get all pokemons, to achieve this you can do it like this:
const [pokemonData, setPokemonData] = useState<any>([]);
const fetchPokemons = async () => {
const data = await fetch(`https://pokeapi.co/api/v2/pokemon?offset=0&limit=153"`);
const pokemons = await data.json();
setPokemonData(pokemons.results);
};
useEffect(() => {
(async () => {
await fetchPokemons();
})();
}, []);
Also this will guarantee that you get data always in the same way. You will not face any race conditions and you won't any unnecessary api calls.

How to conditionally render a list item based on if id matches a websocket object's id

My goal is to use the button on the page to open a websocket connection, subscribe to the ticker feed, then update each list item's price based on the ID of the list item. Currently I have a list that maps through the initial API call's response and saves each object's ID to an array, which in turn is used to build a <li> for each ID. This creates 96 list items. I have also gotten the price to update live via a <p> element in each <li>.
I am having trouble targeting the price for just the matching row ID to the incoming data object's ID so that only that matching row is re-rendered when it gets a match. Below is my code:
ProductRow.js
import React from 'react';
export default function ProductRow(props) {
return <li key={props.id}><p>{ props.id }</p><p>{props.price}</p></li>;
}
WatchList.js
import React, { useState, useEffect, useRef } from "react";
import { Button } from 'react-bootstrap';
import ProductRow from "./ProductRow";
export default function WatchList() {
const [currencies, setcurrencies] = useState([]);
const product_ids = currencies.map((cur) => cur.id);
const [price, setprice] = useState("0.00");
const [isToggle, setToggle] = useState();
const ws = useRef(null);
let first = useRef(false);
const url = "https://api.pro.coinbase.com";
useEffect(() => {
ws.current = new WebSocket("wss://ws-feed.pro.coinbase.com");
let pairs = [];
const apiCall = async () => {
await fetch(url + "/products")
.then((res) => res.json())
.then((data) => (pairs = data));
let filtered = pairs.filter((pair) => {
if (pair.quote_currency === "USD") {
return pair;
}
});
filtered = filtered.sort((a, b) => {
if (a.base_currency < b.base_currency) {
return -1;
}
if (a.base_currency > b.base_currency) {
return 1;
}
return 0;
});
setcurrencies(filtered);
first.current = true;
};
apiCall();
}, []);
useEffect(() => {
ws.current.onmessage = (e) => {
if (!first.current) {
return;
}
let data = JSON.parse(e.data);
if (data.type !== "ticker") {
return;
}
setprice(data.price);
console.log(data.product_id, price);
};
}, [price]);
const handleToggleClick = (e) => {
if (!isToggle) {
let msg = {
type: "subscribe",
product_ids: product_ids,
channels: ["ticker"]
};
let jsonMsg = JSON.stringify(msg);
ws.current.send(jsonMsg);
setToggle(true);
console.log('Toggled On');
}
else {
let unsubMsg = {
type: "unsubscribe",
product_ids: product_ids,
channels: ["ticker"]
};
let unsub = JSON.stringify(unsubMsg);
ws.current.send(unsub);
setToggle(false);
console.log('Toggled Off');
}
};
return (
<div className="container">
<Button onClick={handleToggleClick}><p className="mb-0">Toggle</p></Button>
<ul>
{currencies.map((cur) => {
return <ProductRow id={cur.id} price={price}></ProductRow>
})}
</ul>
</div>
);
}
App.js
import React from "react";
import WatchList from "./components/Watchlist";
import "./scss/App.scss";
export default class App extends React.Component {
render() {
return (
<WatchList></WatchList>
)
}
}
Initialize the price state to be an empty object i.e. {}. We'll refer the price values by the the product_id on getting the response from websocket
// Initialize to empty object
const [price, setprice] = useState({});
...
// Refer and update/add the price by the product_id
useEffect(() => {
ws.current.onmessage = (e) => {
if (!first.current) {
return;
}
let data = JSON.parse(e.data);
if (data.type !== "ticker") {
return;
}
// setprice(data.price)
setprice(prev => ({ ...prev, [data.product_id]: data.price}));
console.log(data.product_id, price);
};
}, [price]);
Render your ProductRows as
<ul>
{currencies.map((cur) => {
return <ProductRow key={cur.id} id={cur.id} price={price[cur.id]}></ProductRow>
})}
</ul>
You don't have to manage anykind of sorting or searching for the relevant prices for products.

React: Passing up data from child component into parent does not update values in parent

// Edit --
This may help:
Project Hatchways
Link to issue -
Issue
As the codes stands right now, the results from the tags still aren't rendering results.
I have a component App.js that renders some children. One of them is 2 search bars. The second search bar TagSearch is supposed to render results from tag creation. What I'm trying to do is pass data from Student where the tags live, and pass them up to the App component in order to inject them into my Fuse instance in order for them to be searched. I have tried to create a function update in App.js and then pass it down to Student.js in order for the tags to update in the parent when a user searches the tags. For some reason, I'm getting a TypeError that states update is not a function.
I put in console logs to track where the tags appear. The tags appear perfectly fine in Student.js, but when I console log them in App.js, the tags just appear as an empty array which tells me they aren't being properly passed up the component tree from Student.js to App.js.
// App.js
import axios from "axios";
import Fuse from "fuse.js";
import Student from "./components/Student";
import Search from "./components/Search";
import TagSearch from "./components/TagSearch";
import "./App.css";
function App() {
const [loading, setLoading] = useState(true);
const [students, setStudents] = useState([]);
const [query, updateQuery] = useState("");
const [tags, setTags] = useState([]);
const [tagQuery, setTagQuery] = useState("");
console.log("tags from app: ", tags);
const getStudents = async () => {
setLoading(true);
try {
const url = `private url for assignment`;
const response = await axios.get(url);
setStudents(response.data.students);
setLoading(false);
} catch (err) {
console.log("Error: ", err);
}
};
const fuse = new Fuse(students, {
keys: ["firstName", "lastName"],
includeMatches: true,
minMatchCharLength: 2,
});
const tagFuse = new Fuse(tags, {
keys: ["text", "id"],
includesMatches: true,
minMatchCharLength: 2,
});
function handleChange(e) {
updateQuery(e.target.value);
}
function handleTags(e) {
setTagQuery(e.target.value);
}
const results = fuse.search(query);
const studentResults = query ? results.map((s) => s.item) : students;
const tagResults = tagFuse.search(tagQuery);
const taggedResults = tagQuery ? tagResults.map((s) => s.item) : tags;
const update = (t) => {
t = tags; // changed this to make sure t is tags from this component's state
setTags(t);
};
useEffect(() => {
getStudents();
}, []);
if (loading) return "Loading ...";
return (
<div className="App">
<main>
<Search query={query} handleChange={handleChange} />
<TagSearch query={tagQuery} handleTags={handleTags} />
{studentResults &&
studentResults.map((s, key) => <Student key={key} students={s} update={update} />)}
{taggedResults &&
taggedResults.map((s, key) => (
<Student key={key} students={s} update={update} />
))}
</main>
</div>
);
}
export default App;
// Student.js
import Collapsible from "../components/Collapsible";
import findAverage from "../helpers/findAverage";
import Styles from "../styles/StudentStyles";
const KeyCodes = {
comma: 188,
enter: 13,
};
const delimiters = [KeyCodes.comma, KeyCodes.enter];
const Student = ({ students, update }) => {
const [isOpened, setIsOpened] = useState(false);
const [tags, setTags] = useState([]);
const collapse = () => {
setIsOpened(!isOpened);
};
const handleDelete = (i) => {
const deleted = tags.filter((tag, index) => index !== i);
setTags(deleted);
};
const handleAddition = (tag, i) => {
setTags([...tags, tag]);
};
useEffect(() => {
update(tags);
}, []);
return (
<Styles>
<div className="student-container">
<img src={students.pic} alt={students.firstName} />
<div className="student-details">
<h1>
{students.firstName} {students.lastName}
</h1>
<p>Email: {students.email}</p>
<p>Company: {students.company}</p>
<p>Skill: {students.skill}</p>
<p>Average: {findAverage(students.grades)}</p>
<Collapsible
students={students}
delimiters={delimiters}
handleDelete={handleDelete}
handleAddition={handleAddition}
isOpened={isOpened}
tags={tags}
/>
</div>
</div>
<button onClick={collapse}>+</button>
</Styles>
);
};
export default Student;
Ciao, try to call update function every time you update tags in Student. Something like this:
const handleDelete = (i) => {
const deleted = tags.filter((tag, index) => index !== i);
setTags(deleted);
update(deleted);
};
const handleAddition = (tag, i) => {
let result = tags;
result.push(tag);
setTags(result);
update(result);
};
In this way, every time you change tags in Student, you will update App state.
An alternative could be use useEffect deps list. In Student, modify useEffect like this:
useEffect(() => {
update(tags);
}, [tags]);
This means that, every time tags will update, useEffect will be triggered and update function will be called.

Modifying object inside array with useContext

I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.

Resources