How do I initiate button based on status from Firestore database? - javascript

I am trying to initiate the button to show the current(true/false) status of the item.
In this case, shown as "Taken" if true or "Returned" if false.
So the initial state could be true or false based on the data from Firestore, and with a click on the button, it can be toggled to the other state.
It seems that my button does not take in the current state. What did I done wrong with my code?
function App() {
const [toys, setToys] = useState([]);
const toysCollectionRef = collection(db,"toys");
const [newAvailability, setAvailability] = useState([]);
const toggleStatus = async (toyID, availability) => {
const userDoc = doc(db,"toys",toyID);
const newFields = {availability: true}
await updateDoc(userDoc, newFields);
window.location.reload(false);
};
useEffect(() => {
const getToys = async () => {
const data = await getDocs(toysCollectionRef);
setToys(data.docs.map((doc) => ({...doc.data(), id:doc.id })))
};
getToys();
}, []);
return (
<div className="App">
<table>
<tr><td>Availability</td></tr>
{toys.map((toy) => {
return (<tr>
<td>{Boolean(toy.availability).toString()}</td>
<td><button onClick={() =>
{toggleStatus(toy.id, toy.availability)}}>
Update Toy Status</button>
{toggleStatus ? "Returned" : "Taken"}
</td>
</tr>)
})}
</table>
</div>
);
}
export default App;

In toggleStatus() you have hard-coded availability to always be true:
const newFields = {availability: true}
You likely want that to be:
const newFields = {availability: availability}
Also, once you update your document you are not re-fetching the values from Firestore.
So you either need to:
handle that as part of your client update -- by updating the toys state variable when you update the data in Firestore, or
(much better yet!) use onSnapshot() instead of getDocs() so that your app is constantly fetching the latest data from the query via "realtime listeners".
If you decide to go with the first approach, you might try:
const toggleStatus = async (toyID, availability) => {
const userDoc = doc(db,"toys",toyID);
const newFields = {availability: true}
await updateDoc(userDoc, newFields);
setToys(curVal => {
let newVal = curVal.map(toy => {
if (toy.id === toyID) {
return {...toy, availability: availability}
} else {
return {...toy}
}
});
return newVal;
});
window.location.reload(false);
};
where you are updating both the FS document (updateDoc()) and the local state variable holding that same data (setToys()).

Related

Only one item is added in state when adding multiple with multiple setState calls

For learning purposes, I'm creating an e-shop, but I got stuck with localStorage, useEffect, and React context. Basically, I have a product catalog with a button for every item there that should add a product to the cart.
It also creates an object in localStorage with that item's id and amount, which you select when adding the product to the cart.
My context file:
import * as React from 'react';
const CartContext = React.createContext();
export const CartProvider = ({ children }) => {
const [cartProducts, setCartProducts] = React.useState([]);
const handleAddtoCart = React.useCallback((product) => {
setCartProducts([...cartProducts, product]);
localStorage.setItem('cartProductsObj', JSON.stringify([...cartProducts, product]));
}, [cartProducts]);
const cartContextValue = React.useMemo(() => ({
cartProducts,
addToCart: handleAddtoCart, // addToCart is added to the button which adds the product to the cart
}), [cartProducts, handleAddtoCart]);
return (
<CartContext.Provider value={cartContextValue}>{children}</CartContext.Provider>
);
};
export default CartContext;
When multiple products are added, then they're correctly displayed in localStorage. I tried to log the cartProducts in the console after adding multiple, but then only the most recent one is logged, even though there are multiple in localStorage.
My component where I'm facing the issue:
const CartProduct = () => {
const { cartProducts: cartProductsData } = React.useContext(CartContext);
const [cartProducts, setCartProducts] = React.useState([]);
React.useEffect(() => {
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
}
}
)();
}, []);
console.log('cartProducts', cartProducts);
return (
<>
<pre>
{JSON.stringify(cartProductsData, null, 4)}
</pre>
</>
);
};
export default CartProduct;
My service file with fetchProductById function:
const domain = 'http://localhost:8000';
const databaseCollection = 'api/products';
const relationsParams = 'joinBy=categoryId&joinBy=typeId';
const fetchProductById = async (id) => {
const response = await fetch(`${domain}/${databaseCollection}/${id}?${relationsParams}`);
const product = await response.json();
return product;
};
const ProductService = {
fetchProductById,
};
export default ProductService;
As of now I just want to see all the products that I added to the cart in the console, but I can only see the most recent one. Can anyone see my mistake? Or maybe there's something that I missed?
This looks bad:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts([...cartProducts, fetchedProduct]);
});
You run a loop, but cartProducts has the same value in every iteration
Either do this:
Object.values(retrievedProducts).forEach(async (x) => {
const fetchedProduct = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...cartProducts, fetchedProduct]);
});
Or this:
const values = Promise.all(Object.values(retrievedProducts).map(x => ProductService.fetchProductById(x.id)));
setCartProducts(values)
The last is better because it makes less state updates
Print the cartProducts inside useEffect to see if you see all the data
useEffect(() => {
console.log('cartProducts', cartProducts);
}, [cartProducts]);
if this line its returning corrects values
const productsObj = localStorage.getItem('cartProductsObj');
then the wrong will be in the if conditional: replace with
(async () => {
const productsObj = localStorage.getItem('cartProductsObj');
const retrievedProducts = JSON.parse(productsObj);
if (productsObj) {
Object.values(retrievedProducts).forEach(async (x) => {
const fetched = await ProductService.fetchProductById(x.id);
setCartProducts(cartProducts => [...fetched, fetchedProduct]);
});
}
}
Issue
When you call a state setter multiple times in a loop for example like in your case, React uses what's called Automatic Batching, and hence only the last call of a given state setter called multiple times apply.
Solution
In your useEffect in CartProduct component, call setCartProducts giving it a function updater, like so:
setCartProducts(prevCartProducts => [...prevCartProducts, fetchedProduct]);
The function updater gets always the recent state even though React has not re-rendered. React documentation says:
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.

Requiring a Hook to update on state change

So this is a component I have in React JS
const EnterSetData = ({ user_card_set }) => {
const [cardlist, setCardlist] = useState({ data: [] });
let setlist= useGetSetDB();
//Save and reset form
const sendSets = () => {
const dupes = findDuplicates(setlist, cardlist);
if (dupes.length > 0) {
saveSets(setlist)
setCardlist({ data: [] });
} else {
handleDupes(dupes);
}
};
useEffect(() => {
setCardlist(card_set);
}, [card_set]);
return (
<>
{cardlist.data.map((card, key) => (
<a> .......</a>))}
<button
type="button"
onClick={sendSets}
>
Save Cards
</button>
</div>
The main issue is while data is saved, there is a major bug. useGetSetDB() is used to fetch the entire set list from an API. After saving, I use setCardlist to update the component state. It is here that I notice that setlist does not change. The new entries are not there and this causes findDuplicates to fail. My DB checks for duplicates, and thus console.logs will show that the inserts failed. How do I force useGetSetDB to update on every state change.This is useGetSetDB
const useGetSetDB = () => {
const[state,setState] = useState([]);
const fetchData = async () => {
const data = await fetch("/sets");
const res = await data.json();
return res;
};
useEffect(()=>{
fetchData().then(response => {setState(response)})
},[])
return state
}
export default useGetSetDB;
You can pass cardlist to useGetSetDB like below
const [cardlist, setCardlist] = useState({ data: [] });
let setlist = useGetSetDB(cardlist); //whenever cardlist gets updated, we trigger side-effect again
You also need to modify useGetSetDB to align with state change
const useGetSetDB = (cardlist) => {
const[state,setState] = useState([]);
const fetchData = async () => {
const data = await fetch("/sets");
const res = await data.json();
return res;
};
useEffect(()=>{
fetchData().then(response => {setState(response)})
}, [cardlist]) //add `cardlist` to dependency list
return state
}
export default useGetSetDB;

Wait Until Data is Fetched from MongoDB React JS

I'm creating a Quiz App. This is the Quiz Page Code. Quiz DB Document contains a QuizQuestions Array which has question ids. Then I fetch specific question from MCQ/Question DB. Fetching MCQ takes time and when I console.log fetched data. First and second time data in undefined then its viewable. Due to this I'm unable to display it as it cause TypeError: Cannot read properties of undefined mcqOptions How can I fix this?
`
import React, { useEffect, useState } from "react";
import shuffleMcq from "../components/shuffleMcq";
const QuizTakePage = ({ match }) => {
const reqUrl = match.params.path; //Getting Quiz ID From URL
const [quizInfo, setQuizInfo] = useState({
"q_title":"",
"q_seo_description":"",
"q_questions":[],
"q_tags":[],
"m_subject":"", })
//const fetchQuizData = fetch();
useEffect(() => {
const fetchQuizData = async () => {
const reqApiUrl = '/quiz/api/qid/'+reqUrl;
const fetchedApiResult = await fetch(reqApiUrl);
const resultJson = await fetchedApiResult.json();
setQuizInfo(resultJson);
}
fetchQuizData(); }, []);
// Quiz Data START
const quizTitle = quizInfo.q_title;
const quizDesc = quizInfo.q_seo_description;
const quizQuestions = quizInfo.q_questions;
// Quiz Data END
// MCQ Data START
const [currentQuestion, setCurrentQuestion] = useState(0);
const requestedQuestion = quizQuestions[currentQuestion];
const [mcqInfo, setMcqInfo] = useState({
"m_title":"",
"m_question":"",
"m_alternatives":[],
"m_language":"", })
useEffect(() => {
const fetchApiResponse = async () => {
const reqApiUrl = '/mcq/api/mid/'+requestedQuestion;
const fetchedApiResult = await fetch(reqApiUrl);
const resultJson = await fetchedApiResult.json();
setMcqInfo(resultJson);
}
fetchApiResponse(); }, [requestedQuestion]);
//const mcqLanguage = mcqInfo.m_language;
const mcqQuestion = mcqInfo.m_question;
const mcqOptions = mcqInfo.m_alternatives;
console.log(mcqOptions);
return (
<>
<h1>{quizTitle}</h1>
<p>{quizDesc}</p>
</>
);
};
export default QuizTakePage;
The reason you're getting undefined for the first and second time is due to the fact that useEffect would've not been executed by then. useEffect runs when the component is rendered and mounted for the first time, and then subsequent executions are made when there is a change in dependency array (If there are any dependencies).
You could get rid of the error by rendering the dynamic content conditionally, i.e, displaying it when the data has been fetched.
return (
<>
<h1>{quizTitle.length>0 ? quizTitle : "Loading Question"}</h1>
<p>{quizDesc.length>0 ? quizDesc: "Loading Description"}</p>
<ul>
{mcqOptions && mcqOptions.length>0 && mcqOptions.map(option=>{
return(<li key={Math.random()}>{option}</li>) //Using Math.random() for key to ensure all the mapped items have an unique key
}
}
</ul>
</>
);
Alternatively, if your mcqOptions is an array of objects you can map it accordingly, for instance, something like this,
<ul>
{mcqOptions && mcqOptions.length>0 && mcqOptions.map(mcqOption=>{
return(<li key={mcqOption.id}>{mcqOption.text}</li>) //Use the properties accordingly, this is an example only.
}
}
</ul>

How to trigger a function automatically when the data in an array changes?

i just started React js and im trying to create a simple recipe web with API.
I am trying to create a page that will display the data of favorite recipes from an array in local storage using map() like below.
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
const refreshData = () => {
const existedFavRecipe = localStorage.getItem("FavRecipes");
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}
return (
<FavRecipesContainer>
{recipeArray.map( e => (
<>
<FavRecipeImage src ={e.image} />
<FavRecipeTitle>{e.title}</FavRecipeTitle>
</>
))}
</FavRecipesContainer>
)
}
The problem is I want the function of refreshData to get triggered automatically everytime the data in the array changes because i will create a delete button that can delete the favorite recipes. I am thinking of using useEffect() but I dont know how to do it. Is there any suggestion to solve this? Would appreciate it!
I want the function of refreshData to get triggered automatically everytime the data in the array changes because i will create a delete button that can delete the favorite recipes..
The problem is that there is no event that fires when local storage is changed by other code in the same window (the storage event only fires when storage is changed in other windows).
There are dodgy solutions like these, but really just make sure that your deletion code calls refreshData as part of its logic. Or actually, you don't even need refreshData, you could maintain the array locally in the component and just echo it to local storage:
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
// Initial data load
useEffect(() => {
const existedFavRecipe = localStorage.getItem("FavRecipes");
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}, []);
// Deletion
const deleteRecipe = (recipe) => {
setRecipeArray(recipes => {
recipes = recipes.filter(r => r !== recipe);
localStorage.setItem("FavRecipes", JSON.stringify(recipes));
return recipes;
});
};
return (
<FavRecipesContainer>
{recipeArray.map( recipe => (
<>
<FavRecipeImage src ={recipe.image} />
<FavRecipeTitle>{recipe.title}</FavRecipeTitle>
<button onClick={() => deleteRecipe(recipe)}>X</button>
</>
))}
</FavRecipesContainer>
);
};
If you also want to listen for changes in other windows (users do that):
const FavRecipesKey = "FavRecipes";
const FavRecipes = () => {
const [recipeArray, setRecipeArray] = useState([]);
// Initial data load and watch for changes in other windows
useEffect(() => {
function refreshData() {
const existedFavRecipe = localStorage.getItem(FavRecipesKey);
const data = existedFavRecipe !== null ? JSON.parse(existedFavRecipe) : [];
setRecipeArray(data);
}
function storageEventHandler({key}) {
if (key === FavRecipesKey) {
refreshData();
}
}
refreshData();
window.addEventListener("storage", storageEventHandler);
return () => {
window.removeEventListener("storage", storageEventHandler);
};
}, []);
// Deletion
const deleteRecipe = (recipe) => {
setRecipeArray(recipes => {
recipes = recipes.filter(r => r !== recipe);
localStorage.setItem(FavRecipesKey, JSON.stringify(recipes));
return recipes;
});
};
return (
<FavRecipesContainer>
{recipeArray.map( recipe => (
<>
<FavRecipeImage src ={recipe.image} />
<FavRecipeTitle>{recipe.title}</FavRecipeTitle>
<button onClick={() => deleteRecipe(recipe)}>X</button>
</>
))}
</FavRecipesContainer>
);
};
This is a tough one. The closest one in React camp is useMutableSource. https://github.com/reactjs/rfcs/blob/main/text/0147-use-mutable-source.md
However useMutableSource is a bit too advanced. So maybe we should think of the problem in another way. For instance, if you can know the time or component who invokes localStorage.setItem, then you can turn it in a context.
Define a context
Create a file that you can share to other components.
const RecipeContext = React.createContext()
export default RecipeContext
Import it to set it
When you want to set the content, import the context and write it via current.
import RecipeContext from './RecipeContext'
const AComponent = () => {
const recipe = React.useContext(RecipeContext)
recipe.current = recipeArray
}
Import it to read it
When you want to read out the current value, import the context and read it via current.
import RecipeContext from './RecipeContext'
const BComponent = () => {
const recipe = React.useContext(RecipeContext)
const onClick = () => {
console.log(recipe.current)
}
}
You should be able to use RecipeContext as a "global" variable similar to localStorage. Even better if you have any default value, you can set it at the time you create it.
const RecipeContext = React.createContext(defaultRecipeArray)
You don't even need a provider <RecipeContext.Provider />, because you are using it as a "global" context, a very special usage.

Multiple rendering problem and how can I use the useEffect here

Filtering data by using this function, if I am calling this function in useEffect than its pushes to search results and not working well.
const AdvanceSearch = (props) => {
const [region, setRegion] = useState("");
const [searchStuhl, setSearchStuhl] = useState("");
const filterData = (async ()=> {
const filtereddata = await props.data.filter((item) => {
return (
item.region.toLowerCase().includes(region.toLowerCase())
&& item.stuhl.toLowerCase().includes(searchStuhl.toLowerCase())
)}
) await props.history.push({
pathname: '/searchResults/',
state:
{
data:filtereddata
}
})
})
//If the props. history.push is pass here instead of the above function then its sending the empty array and not the filtered data
const handleSubmit = async (e) => {
e.preventDefault()
await filterData();
}
when you are changing the navigation URL with some data and there is multiple rendering then the following problem would be there.
Check your route configuration for the path. is it configured to hold the changed path: in this scenario, you get fluctuated UI or we can say multiple renders
yes you can use useEffect hooks to change the path and set the data here is the peace of code. here whenever your props.data will be changed filteredData will run and it will return the value when data will be available.
const filteredData = useCallback(() => {
if(props.data){
const filteredData = props.data.filter((item) => (
item.region.toLowerCase().includes(region.toLowerCase())
&&item.stuhl.toLowerCase().includes(searchStuhl.toLowerCase())
));
return filteredData
}
},
[props && props.data]);
useEffect(()=> {
const data = filteredData();
if(data){
props.history.push({
pathname:'/search-results',
state:{data}
});
}
},[filteredData])
Try to remove async / await from the function. You don't need them to filter an array.

Categories

Resources