How to update React Components from react context - javascript

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

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.

How do I grab Contract Balance with useEffect?

Situation
I have created a smart contract and imported the abi and contract address into my react application.
I have successfully accessed the methods and options within my react app. I used useEffect with no issues.
Problem
However, I am struggling to get the balance of the contract. I have tried everything I understand with no result.
This code works fine
import web3 from "./web";
import { useState, useEffect } from "react";
import Lottery from "./Lottery";
const [isManager, setManager] = useState(
"<em>Loading manager address...</em>"
);
useEffect(() => {
const getManager = async () => {
const mangWallet = await Lottery.methods.manager().call();
console.log(mangWallet);
setManager(mangWallet);
};
getManager();
}, []);
This does NOT work - the balance remains 0. More importantly, the console log doesn't even show 0. It's as if the entire getBalance function is not running?
const [balance, setBalance] = useState(0);
useEffect(() => {
const getBalance = async () => {
const contBal = await web3.eth.getBalance(Lottery.contract_address);
setBalance(contBal);
console.log(balance);
};
getBalance();
}, []);
A few things to be aware of:
contract_address is holding the smart contract address on-chain, stored in the Lottery.js file
these two code snippets are in one file - app.js. Meaning there are two useEffect functions.
I'm not sure what your problem. But I think this async const contBal = await web3.eth.getBalance(Lottery.contract_address); can throw exception. So I think you need to add try catch like this:
const [balance, setBalance] = useState(0);
useEffect(() => {
const getBalance = async () => {
try {
const contBal = await web3.eth.getBalance(Lottery.contract_address);
setBalance(contBal);
console.log(balance);
} catch(error) {
console.log(error);
}
};
getBalance();
}, []);
About the console.log(balance), it alway log 0. Because it's behavior of useEffect. So you need to create another useEffect if you want to see the balance value change like this:
useEffect(() => { console.log(balance) }, [balance])

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.

Pagination on API requests in Javascript/React?

I have an app that fetches data from a movie API. It returns 20 items from page 1.
How would I go about adding the ability for pagination and allowing user to click a button that increases the page number value and returns the items from that page?
Here's my API call:
export const API_KEY = process.env.REACT_APP_MOVIES_API;
export const baseURL = 'https://api.themoviedb.org/3/movie/';
export const language = '&language=en';
export const region = '&region=gb';
export const currentPage = 1;
export const fetchTopRatedMovies = async () => {
try {
const response = await fetch(
`${baseURL}top_rated?api_key=${API_KEY}${language}&page=${currentPage}${region}`
);
const data = await response.json();
console.log('TOP RATED', data);
return data;
} catch (err) {
console.log(err);
}
};
I'm thinking I need to add 1 to currentPage on request however I'm unsure how to set this up.
The function is called using useEffect in React in a functional component.
const [apiData, setApiData] = useState([]);
const [isLoading, setLoading] = useState(false);
const { results = [] } = apiData;
useEffect(() => {
setLoading(true);
fetchTopRatedMovies().then((data) => setApiData(data));
setLoading(false);
}, []);
You need to make currentPage a param of fetchTopRatedMovies, so that you can pass a dynamic value from your functional component.
You should control the current viewed page in the state of your functional component like this:
const [currentPage, setCurrentPage] = useState(1);
Then you can add a button to the rendering of the functional component that in the onClick handler sets the new value of currentPage and triggers the API call. Approximately like this:
<MyButton onClick={() => {
setCurrentPage(currentPage + 1);
fetchTopRatedMovies(currentPage).then(data => setApiData(data));
}}>
I say approximately because instead of doing immediately the call to fetchTopRatedMovies you could leverage useEffect to re-run the API request on each state / prop change. Or even better, trigger the API request using useEffect only when there's a meaningful state / prop change.
The fetchTopRatedMovies method should be improved like this:
export const fetchTopRatedMovies = async (pageNumber) => {
try {
const response = await fetch(
`${baseURL}top_rated?api_key=${API_KEY}${language}&page=${pageNumber}${region}`
);
const data = await response.json();
console.log('TOP RATED', data);
return data;
} catch (err) {
console.log(err);
}
};
This approach can be extended to all the other params of your API call.
Hope this helps!
Usually it's made by adding currentPage to the state, like
const [currentPage, setCurrentPage ] = useState(1);
So when you want to change it, via click or scroll, use setCurrentPage and in your api call it'll still use currentPage but now it'll reference the one in the state.

Categories

Resources