Wait Until Data is Fetched from MongoDB React JS - javascript

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>

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.

React useState array empty on initial load but after editing code while app is running array fills?

This is going to be really hard to explain, but here goes. I am building a React card grid with a filter. The data is pulled from an MySQL AWS API I built. The .tags property is JSON with an array that stores each tag associated with the card. I have written Javascript in App.jsx to turn this JSON into an object, and then store every unique tag in a piece of state. See code below:
//App.jsx
import { useEffect, useState } from 'react';
import '../assets/css/App.css';
import Card from './Card';
import Filter from './Filter'
import {motion, AnimatePresence} from 'framer-motion'
function App() {
const [cards, setCards] = useState([]);
const [filter, setFilter] = useState([]);
const [activeFilter, setActiveFilter] = useState("all");
const [tags,setTags] = useState([]);
useEffect(() => {
fetchData();
}, []);
/*useEffect(() => {
console.log(tags);
console.log(activeFilter);
}, [activeFilter,tags]);
*/
const getTags = () => {
let tags = [];
cards.forEach((card) => {
let obj = JSON.parse(card.tags);
obj.forEach((tag) => {
if (!tags.includes(tag)) {
tags.push(tag);
}
});
});
setTags(tags);
}
const fetchData = async () => {
const data = await fetch("<<api>>");
const cards = await data.json();
setCards(cards);
setFilter((cards));
getTags();
}
return (
<div className="App">
<Filter
cards={cards}
setFilter={setFilter}
activeFilter={activeFilter}
setActiveFilter={setActiveFilter}
/>
<motion.div layout className="Cards">
<AnimatePresence>
{filter.map((card) => {
return <Card key={card.id} card={card}/>;
})}
</AnimatePresence>
</motion.div>
</div>
);
}
export default App;
The problem that I am having is that when I run the app initially, the tags state is empty when inspecting from React Dev tools. However, when I keep the app running, and then add something like a console.log(tags); before setTags(tags) is called in the getTags() function, the data suddenly appears in the state. If someone could explain why the state seems to be empty even though I am updating it on the initial render that would be really appreciated.
You are running getTags on empty array. setCards doesn't set the const variable instantly. New values will be present in the next render cycle.
Try adding cards param
const getTags = (cards) => {
let tags = [];
cards.forEach((card) => {
let obj = JSON.parse(card.tags);
obj.forEach((tag) => {
if (!tags.includes(tag)) {
tags.push(tag);
}
});
});
setTags(tags);
}
And use it like this:
const fetchData = async () => {
const data = await fetch("API url");
const cards = await data.json();
setCards(cards);
setFilter((cards));
getTags(cards);
}

How to update React Components from react context

How to increment a count in react context by that I need to change the count value based on some events that happens On ContextCount.js file
let count = 0;
const createSale = async (url, formInputPrice, isReselling, id) => {
// connect to smart contract
const web3Modal = new Web3Modal();
const connection = await web3Modal.connect();
const provider = new ethers.providers.Web3Provider(connection);
const signer = provider.getSigner();
// value of wei of zeo of to convert to it that what metamask read
const price = ethers.utils.parseUnits(formInputPrice, 'ether');
// calling func() from contract takes time so use await
const contract = fetchContract(signer);
count += 1;
const listingPrice = await contract.getListingPrice();
const transaction = !isReselling
? await contract.createToken(url, price, { value: listingPrice.toString() })
// else it belong to this
: await contract.resellToken(id, price, { value: listingPrice.toString() });
await transaction.wait();
};
<NFTContext.Provider value={{ count }}>
{children}
</NFTContext.Provider>
On React Components folder i need to pass the updated value of count when evener a createsale function is happen
So i use on components folder i created a countupdate.jsx On there import React from 'react'; i imported the context file then i use
const { count } = useContext(ContextCount);
const updateIt = () => (
<div>{count}</div>
);
export default updateIt;
So I need to show the updated value from ContextCount.js to Components file updateIt.jsx Does anyone know how to implement this?
It is not updated because you are using the hook wrong. Here is where you can learn all you need about hooks. The context hook should be used inside your component, like this:
const updateIt = () => {
const { count } = useContext(ContextCount);
return (
<div>{count}</div>
);
};
And here is the answer on how to update data in context from its consumer

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.

React.js: How to create loadMore feature onClick?

I am trying to implement a small app with loadMore button.
By default, it should output 3 data, and show 3 more data each time the user clicks the load more button.
Trying multiple ways I came up with this solution suggested by this article.
Here is the codesandox link
App.js
let arrayForHoldingStories = [];
const [topStoryIds] = useStories();
const [storiesToShow, setStoriesToShow] = useState([]);
const [next, setNext] = useState(3);
const loopWithSlice = (start, end) => {
const slicedStories = topStoryIds.slice(start, end);
arrayForHoldingStories = [...arrayForHoldingStories, ...slicedStories];
setStoriesToShow([...arrayForHoldingStories, ...slicedStories]);
};
useEffect(() => {
loopWithSlice(0, storiesPerPage);
}, [topStoryIds]);
const handleShowMorePosts = () => {
let loadedMore = next + storiesPerPage;
loopWithSlice(next, loadedMore);
setNext(next + storiesPerPage);
};
But the problem is that merging the initial array and the new array, they have same elements and it throws an error
Warning: Encountered two children with the same key, `26991887`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
I tried to remove duplicates from both arrays via forEach or set functions before merging but didn't work either.
My try
const loopWithSlice = (start, end) => {
const slicedStories = topStoryIds.slice(start, end);
arrayForHoldingStories = [...arrayForHoldingStories, ...slicedStories];
setStoriesToShow([...arrayForHoldingStories, ...slicedStories]); //it throws mentioned error
/* slicedStories.forEach((uniqueId) => { //this will also throw same error
arrayForHoldingStories.forEach((addedId) => {
if (uniqueId !== addedId) {
setStoriesToShow([...slicedStories, ...arrayForHoldingStories]);
}
});
}); */
/* setStoriesToShow([ //this will also throw same error
...new Set([...arrayForHoldingStories, ...slicedStories])
]); */
};
Any help will be appreciated.
Per your description, you don't need to have an end or start arguments to your loop with slice function. You can use the length of the storiesToShow state
import React, { useState, useEffect } from "react";
import Stories from "./Stories";
import useStories from "./hooks/useStories";
function App() {
const storiesPerPage = 3;
let arrayForHoldingStories = [];
const [topStoryIds] = useStories();
const [storiesToShow, setStoriesToShow] = useState([]);
const [next, setNext] = useState(3);
const loopWithSlice = () => {
const toShow = topStoryIds.slice(
storiesToShow.length,
storiesToShow.length + storiesPerPage
);
setStoriesToShow([...storiesToShow, ...toShow]);
};
useEffect(() => {
if (topStoryIds) {
loopWithSlice();
}
}, [topStoryIds]);
const handleShowMorePosts = () => {
let loadedMore = next + storiesPerPage;
loopWithSlice(next, loadedMore);
setNext(next + storiesPerPage);
};
console.log("next", next);
console.log("storiesPerPage", storiesPerPage);
console.log("loopWithSlice", loopWithSlice);
console.log("arrayForHoldingStories", arrayForHoldingStories);
console.log("storiesToShow", storiesToShow);
return (
<>
<div>
{storiesToShow.length > 0 &&
storiesToShow.map((storyId) => (
<Stories storyId={storyId} key={storyId} />
))}
</div>
<button onClick={handleShowMorePosts}>load more</button>
</>
);
}
export default App;
You can check the sanbox here for the full code.

Categories

Resources