I am looking for some help in delaying the display of some calculated data until the state that it relies on is set in a parent component. In the below-simplified example, App has a value set to 0, then some calculations are run and the state is updated.
The problem is that when Calculate is clicked, the calculation which updates the state of the initial val hasn't yet happened, and the final value derived from calcOverUnderPayment is therefore incorrect.
It only displays the correct value on all subsequent clicks.
What could I do to fix this?
Thanks so much in advance.
function App() {
const [val, setVal] = useState(0)
const calculate = () => {
// code to run some calculations which I then set as state
setVal(100)
}
return (
<>
<button onClick={() => calculate()}>Calculate</button>
<Outcome
val={val}
/>
</>
)
}
function Outcome(val) {
const calcOverUnderPayment = (value: number, type: string) => {
if (type === 'under') {
return (value > 0) ? value : 0
} else {
return (value < 0) ? value : 0
}
}
return (
<>
Final Val is {calcOverUnderPayment(val)}
</>
)
}
I have gone through comments of yours on the other answer. You can use another state variable and useEffect hook in your Outcome component.
function App() {
// If it is possible, try to change the initial value to null so that the Outcome component can figure out when did the value change.
const [val, setVal] = useState(null)
const calculate = () => {
// code to run some calculations which I then set as state
setVal(100)
}
return (
<>
<button onClick={() => calculate()}>Calculate</button>
<Outcome
val={val}
/>
</>
)
}
function Outcome({ val }) {
// Use this state to display initial value and update this state instead of calling the function directly in the return
const [valueToDisplay, setValueToDisplay] = useState('YOUR_INITIAL_VALUE_TO_DISPLAY');
const calcOverUnderPayment = (value: number, type: string) => {
if (type === 'under') {
return (value > 0) ? value : 0
} else {
return (value < 0) ? value : 0
}
}
// use useEffect hook to run your function whenever val prop is changed
useEffect(() => {
// If the val prop is null, then it is evident that it is rendering for the first time.
// So, don't run the calcOverUnderPayment() method. This is why "null" is important to provide as your initial state for "val" in App component
if(val !== null) {
const value = calcOverUnderPayment();
// set the state variable here to display
setValueToDisplay(value);
}
},[val]) // Give the val prop as the dependency. This tells useEffect to run whenever val prop is changed.
return (
<>
{/* Instead of calling your function here, use the state variable*/}
Final Val is {valueToDisplay}
</>
)
}
This should work as you intended. Let me know if you encounter any problem or didn't understand it.
As one of the options you could just use conditional rendering, and render your child component, only when state isn't default, in your case something like this would do:
return( //your components here
{val != 0 && <Outcome val={val} />}
//rest of your component
)
Related
It seems I can't access the state from a useEffect() hook in my project. More specifically, I'm successfully fetching some data from an API; then I save those data in the component's state; part of it gets then "published" in a Context, the remaining is passed to child components as props. The problem is that the setState() functions are not updating the state correctly; and I've noticed while debugging that if I put a watch for the state variables, they show up as null even though the promises do fulfill and JS succesfully assigns the correct data to the service variables resMainCityCall, resUrlAltCity1Call and resUrlAltCity2Call. The hook useState() assigns null as a default value to the state variables mainCityData, altCity1Data, altCity2Data, but the state setter functions fail to assign the values fetched to the state, which stay set to null.
Caller.js
import React from 'react';
import { useState, useEffect } from 'react';
import { MainCity } from './MainCity';
import { AltCity } from './AltCity';
export const MainCityContext = React.createContext(
null // context initial value. Let's fetch weather data, making our Caller component the provider. The main city box and the other two boxes will be consumers of this context, aka the data fetched.
);
export const Caller = () =>
{
const [mainCityData, setMainCityData] = useState(null);
const [altCity1Data, setAltCity1Data] = useState(null);
const [altCity2Data, setAltCity2Data] = useState(null);
useEffect(() =>
{
const fetchData = async () =>
{
const urlMainCityBox = "https://api.openweathermap.org/data/2.5/weather?lat=45.0677551&lon=7.6824892&units=metric&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
// const urlTodayBox = "https://pro.openweathermap.org/data/2.5/forecast/hourly?lat=45.0677551&lon=7.6824892&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
// const urlWeekMonthBox = "https://api.openweathermap.org/data/2.5/forecast/daily?lat=45.0677551&lon=7.6824892&cnt=7&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
const urlAltCity1 = "https://api.openweathermap.org/data/2.5/weather?lat=51.5073219&lon=-0.1276474&units=metric&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
const urlAltCity2 = "https://api.openweathermap.org/data/2.5/weather?lat=41.8933203&lon=12.4829321&units=metric&appid=65e03b16f8eb6ba0ef7776cd809a50cd";
let globalResponse = await Promise.all([
fetch(urlMainCityBox),
//fetch(urlTodayBox),
//fetch(urlWeekMonthBox),
fetch(urlAltCity1),
fetch(urlAltCity2)
]);
const resMainCityCall = await globalResponse[0].json();
// const resUrlTodayBoxCall = await globalResponse[1].json();
// const resUrlWeekMonthBoxCall = await globalResponse[2].json();
const resUrlAltCity1Call = await globalResponse[1].json();
const resUrlAltCity2Call = await globalResponse[2].json();
setMainCityData({
"name" : resMainCityCall.name,
"weather" : resMainCityCall.weather[0].main,
"weather_description" : resMainCityCall.weather[0].description,
"icon" : resMainCityCall.weather[0].icon,
"temperature" : resMainCityCall.weather[0].main.temp,
"time" : convertTimeOffsetToDate( resMainCityCall.timezone )
// spot for the forecasted data (paid API on OpenWeather).
});
setAltCity1Data({
"name" : resUrlAltCity1Call.name,
"weather" : resUrlAltCity1Call.weather[0].main,
"weather_description" : resUrlAltCity1Call.weather[0].description,
"icon" : resUrlAltCity1Call.weather[0].icon,
"temperature" : resUrlAltCity1Call.weather[0].main.temp,
"time" : convertTimeOffsetToDate( resUrlAltCity1Call.timezone ) // time attribute is type Date
});
setAltCity2Data({
"name" : resUrlAltCity2Call.name,
"weather" : resUrlAltCity2Call.weather[0].main,
"weather_description" : resUrlAltCity2Call.weather[0].description,
"icon" : resUrlAltCity2Call.weather[0].icon,
"temperature" : resUrlAltCity2Call.weather[0].main.temp,
"time" : convertTimeOffsetToDate( resUrlAltCity2Call.timezone ) // time attribute is type Date
});
console.log("Status updated.");
console.log(mainCityData);
console.log(altCity1Data);
console.log(altCity2Data);
}
fetchData().catch((error) => { console.log("There was an error: " + error)});
}, []); // useEffect triggers only after mounting phase for now.
let mainCityComponent, altCity1Component, altCity2Component = null;
// spot left for declaring the spinner and setting it on by default.
if ((mainCityData && altCity1Data && altCity2Data) != null)
{
mainCityComponent = <MainCity />; // Not passing props to it, because MainCity has nested components that need to access data. It's made available for them in an appropriate Context.
altCity1Component = <AltCity data={ altCity1Data } />
altCity2Component = <AltCity data={ altCity2Data } />
}
// spot left for setting the spinner off in case the state is not null (data is fetched).
return (
<div id="total_render">
<MainCityContext.Provider value={ mainCityData } >
{ mainCityComponent }
</MainCityContext.Provider>
{ altCity1Component }
{ altCity2Component }
</div>
);
}
const convertTimeOffsetToDate = (secondsFromUTC) =>
{
let convertedDate = new Date(); // Instanciating a Date object with the current UTC time.
convertedDate.setSeconds(convertedDate.getSeconds() + secondsFromUTC);
return convertedDate;
}
Since the state stays null, the check
if ((mainCityData && altCity1Data && altCity2Data) != null)
doesn't pass and nothing gets rendered.
There are some issues with your code and probably your logic. I'll list them here and ask questions so you can think about them and answer the best way you think it makes sense.
Too much logic inside useEffect()
As advice, you could save the result from the request and keep that in a state. When that state got updated, you update the respective states. Something like this could work:
const [requestResponseValue, setRequestReponseValue] = useState([]);
const [first, setFirst] = useState();
const [second, setSecond] = useState();
const [last, setLast] = useState();
useEffect(() => {
const initialFetch = async () => {
const allResult = await Promise.all([
fetchFirst(),
fetchSecond(),
fetchLast(),
]);
setRequestReponseValue(allResult);
}
initialFetch();
}, []);
useEffect(() => {
if (requestResponseValue[0] !== undefined) {
setFirst(requestResponseValue[0]);
}
}, [requestResponseValue, setFirst]);
useEffect(() => {
if (requestResponseValue[1] !== undefined) {
setSecond(requestResponseValue[1]);
}
}, [requestResponseValue, setSecond]);
useEffect(() => {
if (requestResponseValue[2] !== undefined) {
setLast(requestResponseValue[2]);
}
}, [requestResponseValue, setLast]);
Log states right after updating them
When you call the setX() function, the respective state will not change right after you called that function. This code will never log the correct value:
const [age, setAge] = useState(20);
useEffect(() => {
setAge(30);
console.log(age);
}, []);
It'll log 20 instead of 30.
Confusion on setting child components
This code:
let mainCityComponent, altCity1Component, altCity2Component = null;
sets the value from:
mainCityComponent to undefined
altCity1Component to undefined
altCity2Component to null
Is that what you want?
Confusion about checking logical operations
Also, this code:
if ((mainCityData && altCity1Data && altCity2Data) != null)
checks for (mainCityData && altCity1Data && altCity2Data) being different from null with just the != operator. If you want to check each one of those, you should write something like this:
if (
mainCityData !== null
&& altCity1Data !== null
&& altCity2Data !== null
)
But the meaning of that is totally different from what you wrote. Also, I can't say for sure but I think those "component" variables will never be rendered as the component API is not expecting them to change and it is not "waiting" for them. Long story short: They are not mutable. (But I'm not 100% sure about that as I didn't test this, specifically)
Even with this, you could write this with a different approach.
Use !! and conditional render
To avoid this specific case, you could write your render function like this:
return (
<div id="total_render">
<MainCityContext.Provider value={ mainCityData } >
{!!mainCityData ? <MainCity /> : null}
</MainCityContext.Provider>
{!!altCity1Data ? <AltCity alt={altCity1Data} /> : null}
{!!altCity2Data ? <AltCity alt={altCity2Data} /> : null}
</div>
);
With that, when the mainCityData, altCity1Data, and altCity2Data states got changed, they'll re-render for you.
I'm using React functional components and here are my codes:
Parent component function:
const calculateAPR = async (val) => {
setIsAprLoading(true);
try {
if (val.addr !== "" && val.date !== null) {
const totalStaking = await someEP.getSomeData(val.addr);
val.staked = totalStaking;
setResData((prevState) => {
return ({
...prevState,
aprRes: val
})
})
setRenderApr(true);
setIsAprLoading(false);
}
else {
setRenderApr(false);
alert(Constants.ADDR_N_DATE_ERR);
}
}
catch (err) {
setRenderApr(false);
console.log(err);
alert(Constants.ADDR_NO_DATA);
}
finally {
setIsAprLoading(false);
}
}
...
return (
...
<QueryAprField text={Constants.CALC_APR_HEADER} onFunction={calculateAPR} isLoading={isAprLoading} />
<CalculateAprField resData={resData.aprRes} onRender={renderApr} />
...
)
Child component 1:
function QueryAprField(props) {
...
const handleQuery = () => {
const verify = verifyDelegatorAddress();
if (verify) {
props.onFunction(queryValue);
}
else {
alert(Constants.ENTER_VALID_DEL_ADDR);
}
}
...handles taking in user inputs and passing it to parent component...
}
Child component 2:
function CalculateAprField(props) {
const aprRes = props.resData;
...
const renderCard = () => {
if (renderData == true) {
const aprInput = setAprInputs(aprRes);
const { staked } = extractAprInput(aprInput);
const apr = parseFloat(calculateAPR(staked, accrued, withdrawn, numOfDays).toFixed(5));
if (isNaN(apr)) {
//How to reset aprRes and ensure that its not using old values
return alert(Constants.APR_AUTO_ERR)
}
return (
<Paper elevation={4}>
...some html and css...
</Paper>
)
}
}
return (
<Box>
{renderCard()}
</Box>
)
I'm trying to enable a situation where, after calculateAPR in the parent component is executed, some data will be passed to child component 2; and in child component 2 in the renderCard function, if the variable apr in child component 2 is NaN then an alert will be triggered. However, the problem I'm facing now is that after the alert is triggered, and when I put in new values and execute calculateAPR again, child component 2 seems to use the old values first before using the new values that are passed down from the parent component. So in other words, I get the alert first and then it uses the new values that are being passed down.
How can I enable the aprRes variable in child component 2, to reset its value after the alert is thrown? So that the alert is not thrown twice?
There is no need to reset the value. component rerenders only on props change.
I see the problem could be with this line. const aprInput = setAprInputs(aprRes); React setState doesn't return anything. Please change it to below and try once.
setAprInputs(aprRes);
(or)
if aprInput is not used anywhere else better extract from aprRes only.
const { staked } = extractAprInput(aprRes);
Incase if setAprInputs is not a setState rather a user-defined function, ensure it is a synchronous function and console.log after the call.
hope this gives you some insight to debug.
I needed a hook to get the previous distinct value of a specific state. It looks like this and it seems to work:
function usePreviousDistinct(state) {
const prevRef = useRef();
useEffect(() => {
prevRef.current = state;
}, [state]);
return prevRef.current;
}
I've also seen there is a usePreviousDistinct hook in the react-use package but the approach is different than mine.
import { useRef } from 'react';
import { useFirstMountState } from './useFirstMountState';
export type Predicate<T> = (prev: T | undefined, next: T) => boolean;
const strictEquals = <T>(prev: T | undefined, next: T) => prev === next;
export default function usePreviousDistinct<T>(value: T, compare: Predicate<T> = strictEquals): T | undefined {
const prevRef = useRef<T>();
const curRef = useRef<T>(value);
const isFirstMount = useFirstMountState();
if (!isFirstMount && !compare(curRef.current, value)) {
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
I wonder if I have not understood something or am missing something. Is my version also correct?
In my test I could not find a difference:
https://codesandbox.io/s/distracted-mayer-zpym8?file=/src/App.js
useEffect() together with useRef() (your version) does not show the latest value.
useRef() without useEffect() gives you the correct value, because it runs synchronously, but doesn't have the advantages that come with asynchronicity.
useEffect() together with useState() gives you the correct value, but might trigger unnecessary renders (adds potentially unnecessary overhead).
Your version looks like it works as expected, because the old value that is shown is the one that you expect to see as the new value. But the actual new value is not the one you want.
Example:
import React, { useState, useEffect, useRef } from 'react';
export const MyComponent = function(props){
const [state, setState ] = useState(0);
return <React.Fragment>
state: { state },<br />
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
<button onClick={ function(){
setState( state + 1 );
} }>
increment
</button>
</React.Fragment>;
};
const usePreviousDistinctUE = function( value ){
const prevRef = useRef();
useEffect(() => {
prevRef.current = value;
console.log('with useEffect, prev:', prevRef.current, ', current:', value);
}, [value]);
return prevRef.current;
};
const usePreviousDistinctR = function( value ){
const prevRef = useRef();
const curRef = useRef( value );
if( curRef.current !== value ){
prevRef.current = curRef.current;
curRef.current = value;
}
console.log('w/o useEffect, prev:', prevRef.current, ', current:', curRef.current);
return prevRef.current;
};
The values shown on the page are the same, but in the console they are different. That means the value in the useEffect() version is changed, it is only not yet shown on the page.
If you just add another hook that updates anything unrelated (leaving everything else unchanged), then the page (might*) magically show the updated value again, because the page is re-rendered and the previously already changed value is shown. The value is now wrong in your eyes, but it is not changed, only shown:
// ...
with useEffect: { usePreviousDistinctUE( state ) },<br />
w/o useEffect: { usePreviousDistinctR( state ) },<br />
anything updated: { useAnythingUpdating( state ) },<br />
// ...
const useAnythingUpdating = function(state){
const [result, setResult ] = useState(0);
useEffect(() => {
setResult( state );
console.log('anything updated');
});
return result;
};
*But you shouldn't rely on something else triggering a re-render. I'm not even sure this would update as expected under all circumstances.
more Details:
useEffect() is triggered at some time when react decides that the prop must have been changed. The ref is changed then, but react will not 'get informed' about a ref change, so it doesn't find it necessary to re-render the page to show the changed value.
In the example without useEffect() the change happens synchronously.
React doesn't 'know' about this change either, but (if everything else runs as expected) there will be always a re-render when necessary anyway (you will have called that function from another function that is rendered at the end).
(Not informing react about the change is basically the point in using useRef(): sometimes you want just a value, under your own control, without react doing magic things with it.)
I have code that is of the following pattern inside a React FunctionComponent:
const MyComponent: React.FunctionComponent<ISomeInterface> = ({ someArray, someFunction }) => {
const [someStateObjVar, setSomeStateObjVar] = React.useState({});
const [isFound, setIsFound] = React.useState(false);
const handleSomeEvent = React.useCallback((someAttribute: string) => {
if (someAttribute) {
setSomeStateObjVar(someArray.find(
(arrayElement) => ( arrayElement.attribute === someAttribute )
);
setIsFound(someStateVar ?? false);
}
}
return ( isFound && someStateObjVar ) ? <FoundMatchingComponent /> ? <ComponentNotFound />;
In the above code, there's always a match for someArray element with someAttribute.
But the problem is MyComponent always renders ComponentNotFound because isFound always evaluates to FALSE at the last line (return statement).
I was able to fix this with the following refactor below (inserting an intermediate variable, but overall the logic remains the same):
const MyComponent: React.FunctionComponent<ISomeInterface> = ({ someArray, someFunction }) => {
const [someStateObjVar, setSomeStateObjVar] = React.useState({});
const [isFound, setIsFound] = React.useState(false);
const handleSomeEvent = React.useCallback((someAttribute: string) => {
let foundElement;
if (someAttribute) {
foundElement = someArray.find(
(arrayElement) => ( arrayElement.attribute === someAttribute )
);
}
if (foundElement) {
setSomeStateObjVar(foundElement);
setIsFound(true);
}
}
return ( isFound && someStateObjVar ) ? <FoundMatchingComponent /> ? <ComponentNotFound />;
With this 2nd version of the code, isFound correctly evaluates to TRUE at the last line, and MyComponent correctly renders FoundMatchingComponent.
Can you please provide an explanation why the first version does not work, while the second one does?
My guess is that the intermediate variable in the second version gives enough time for React to evaluate the isFound variable correctly at the return statement, but I'm not confident this is the explanation. Any advice on improving my code above would be appreciated as well. Thanks.
In the first code snippet, I do not see where someStateVar is defined. If that is the case the variable is undefined, so setIsFound(someStateVar ?? false) will always evaluate as false. Therefore, isFound is false and the return statement will always return <ComponentNotFound />.
Did you intend to have setIsFound(someStateObjVar ?? false)?
I have a child component called First which is implemented below:
function First(props) {
const handleButtonClick = () => {
props.positiveCallback({key: 'positive', value: 'pos'})
props.negativeCallback({key: 'negative', value: '-100'})
}
return (
<div><button onClick={() => handleButtonClick()}>FIRST</button></div>
)
}
And I have App.js component.
function App() {
const [counter, setCounter] = useState({positive: '+', negative: '-'})
const handleCounterCallback = (obj) => {
console.log(obj)
let newCounter = {...counter}
newCounter[obj.key] = obj.value
setCounter(newCounter)
}
const handleDisplayClick = () => {
console.log(counter)
}
return (
<div className="App">
<First positiveCallback = {handleCounterCallback} negativeCallback = {handleCounterCallback} />
<Second negativeCallback = {handleCounterCallback} />
<button onClick={() => handleDisplayClick()}>Display</button>
</div>
);
}
When handleButtonClick is clicked in First component it triggers multiple callbacks but only the last callback updates the state.
In the example:
props.positiveCallback({key: 'positive', value: 'pos'}) // not updated
props.negativeCallback({key: 'negative', value: '-100'}) // updated
Any ideas?
Both are updating the state, your problem is the last one is overwriting the first when you spread the previous state (which isn't updated by the time your accessing it, so you are spreading the initial state). An easy workaround is to split counter into smaller pieces and update them individually
const [positive, setPositive] = useState('+')
const [negative, setNegative] = useState('-')
//This prevents your current code of breaking when accessing counter[key]
const counter = { positive, negative }
const handleCounterCallback = ({ key, value }) => {
key === 'positive' ? setPositive(value) : setNegative(value)
}
You can do that but useState setter is async like this.setState. If you want to base on the previous value you should use setter as function and you can store it in one state - change handleCounterCallback to
const handleCounterCallback = ({key,value}) => {
setCounter(prev=>({...prev, [key]: value}))
}
and that is all. Always if you want to base on the previous state use setter for the state as function.
I recommend you to use another hook rather than useState which is useReducer - I think it will be better for you