How to add loading state in React while useEffect is processing synchronously - javascript

I've got an app with 2 values that user can change stored as state. When they're changed I do some processing in useEffect and also store output of this processing in state and render it.
At this point everything is working perfectly fine, but this processing takes some time and I want to show some loading indicator. I want it to show after button click.
This is simplified version of my app and dumb implementation of this loading indicator - I know why this doesn't work, I just don't know how to write it correctly so I made this just to show what I'm trying to do. Here is the code:
function App() {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
const [output, setOutput] = useState(0);
const [isLoading, setIsLoading] = useState(false); // setting true here won't work,
// because I want to show loading indicator after user change value1 or value2
// and not on inital load
useEffect(() => {
if (!value1 && !value2) {
return;
}
setIsLoading(true);
for (let i = 0; i < 1999999999; i++) {} // some long operations
setOutput(value1 + value2);
setIsLoading(false);
}, [value1, value2]);
return (
<div>
<button onClick={() => setValue1(value1 + 1)}>increment value1</button>
<button onClick={() => setValue2(value2 + 1)}>increment value2</button>
<div>
{value1} + {value2} = {isLoading ? 'please wait...' : output}
</div>
</div>
);
}

You can start the value of isLoading equals true, so, the page will start on loading.
function App() {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
const [output, setOutput] = useState();
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
for (let i = 0; i < 1999999999; i++) {} // some long operations
setOutput(value1 + value2);
setIsLoading(false);
}, [value1, value2]);
return (
<div>
<button onClick={() => setValue1(value1 + 1)}>increment value1</button>
<button onClick={() => setValue2(value2 + 1)}>increment value2</button>
<div>
{value1} + {value2} = {isLoading ? 'please wait...' : output}
</div>
</div>
);
}

Try this piece of code:
<div>
<button
onClick={() => {
setIsLoading(true);
setValue1(value1 + 1);
}}
>
increment value1
</button>
<button
onClick={() => {
setIsLoading(true);
setValue2(value2 + 1);
}}
>
increment value2
</button>
Complete code is here: https://codesandbox.io/s/relaxed-hill-jy1bfm?file=/src/App.js:689-1018

The problem is that it is asynchronous. There may be many different approaches to the solution, but for your simple example I have simple solution:
export default function App() {
const [value1, setValue1] = useState(0);
const [value2, setValue2] = useState(0);
const [output, setOutput] = useState(0);
const isLoading = value1 + value2 !== output;
useEffect(() => {
if (!value1 && !value2) {
return;
}
for (let i = 0; i < 1000; i++) {
console.log(1);
} // some long operations
setOutput(value1 + value2);
}, [value1, value2]);
return (
<div>
<button onClick={() => setValue1(value1 + 1)}>increment value1</button>
<button onClick={() => setValue2(value2 + 1)}>increment value2</button>
<div>
{value1} + {value2} = {isLoading ? "please wait..." : output}
</div>
</div>
);
}

The problem is that between setting isLoading to true and setting it back to false (after the calculation-heavy operation) no rendering happened.
Several approaches now come to my mind; and I'm not sure which one actually works as expected and (from those that do) which one I would pick; so I just share what's on my mind:
approach 1 (adding it to the task queue of the main thread): only setting the loading flag synchronously (and thus returning from user code and handing the control flow back to the current render-run of react), and deliberately triggering the calculation in an asynchronous way
useEffect(() => {
if (!value1 && !value2) return;
setIsLoading(true);
setTimeout(() => {
const result = heavyCalculation(value1, value2);
setOutput(result);
setIsLoading(false);
});
}, [value1, value2]);
approach 2 (adding it to the microtask queue of the main thread): turn it into micro tasks (a.k.a. Promises): when your useEffect just creates and starts a promise object and then "forgets" about it, the control flow is handed back to react. when a promise resolves and changes your component state react will do a re-render. But I suspect this might not bring any change, because afaik the microtask queue runs on the main thread and will run until empty, before react can schedule the re-rendering task. Disclaimer: promises are not my strong suit and I might have fudged that up here.
useEffect(() => {
if (!value1 && !value2) return;
setIsLoading(true);
Promise.resolve().then(() => {
const result = heavyCalculation(value1, value2);
setOutput(result);
setIsLoading(false);
});
}, [value1, value2]);
approach 3 (using flushSync): ensuring the UI is rendered before your next statement. you are not allowed to call flushSync inside useEffect, therefore you need to place the call inside a new task or microtask
useEffect(() => {
if (!value1 && !value2) return;
Promise.resolve()
.then(() => flushSync(() => setIsLoading(true)))
.then(() => {
const result = heavyCalculation(value1, value2);
setOutput(result);
setIsLoading(false);
});
}, [value1, value2]);
or
useEffect(() => {
if (!value1 && !value2) return;
setTimeout(() => {
flushSync(() => setIsLoading(true));
const result = heavyCalculation(value1, value2);
setOutput(result);
setIsLoading(false);
});
}, [value1, value2]);

Related

Change state while waiting for loop to finish

OnClick I want to call a function that sets my loading state to true then do a for loop which will take more than one second and on the last loop set the loading state back to false but in my code the loading state doesn't change as expected. What do I have to fix?
import { useState } from "react"
const Test = () => {
const [loading, setLoading] = useState(false)
const someFunction = () => {
setLoading(true)
const someVeryBigArray = [...]
for (let i = 0; i < someVeryBigArray.length; i++) {
if (i === someVeryBigArray.length - 1) {
setLoading(false)
}
}
}
return (
<button onClick={someFunction} className={`${loading && "text-red-500"}`}>
test
</button>
)
}
export default Test
You need to give the browser time to re-render. If you have a huge blocking loop, React won't be yielding control back to the browser so that it can repaint (or even to itself so that the component can run again with the new state).
While one approach would be to run the expensive function in an effect hook, after the new loading state has been rendered:
const Test = () => {
const [running, setRunning] = useState(false)
useEffect(() => {
if (!running) return;
const someVeryBigArray = [...]
for (let i = 0; i < someVeryBigArray.length; i++) {
// ...
}
setRunning(false);
}, [running]);
return (
<button onClick={() => setRunning(true)} className={running && "text-red-500"}>
test
</button>
)
}
A better approach would be to offload the expensive code to either the server, or to a web worker that runs on a separate thread, so as not to interfere with the UI view that React's presenting.
To be honest if in any case your loop is taking 1 second to run, then this will cost into you app's performance. And this is not the best way to do as well.
The better way would be, If your really want to replicate the delay in you app then you should use setTimeout() using which you delay some action. sharing a code snippet might help you.
JSX
import { useEffect, useState } from "react";
const Test = () => {
const [loading, setLoading] = useState(false);
let timevar = null;
const someFunction = () => {
setLoading(true);
timevar = setTimeout(() => {
setLoading(false); //this will run after 1 second
}, 1000); //1000 ms = 1 second
};
useEffect(() => {
return () => {
//clear time out if in case component demounts during the 1 second
clearTimeout(timevar);
};
});
return (
<button onClick={someFunction} className={`${loading && "text-red-500"}`}>
test
</button>
);
};
export default Test;

What happens when setState is invoked?

I am learning React and having difficulty in understanding the flow of code that is happening.
Here is my code:It is a functional React component
function App() {
const [x, setx] = useState(1);
const [x1, setx1] = useState(1);
const [x2, setx2] = useState(1);
const [x3, setx3] = useState(1);
const [x4, setx4] = useState(1);
const [x5, setx5] = useState(1);
console.log("out3");
const bclick = () => {
setx1(x1 + 1);
setx2(x2 + 1);
setx3(x3 + 1);
setx4(x4 + 1);
setx5(x5 + 1);
setx(x + 1);
console.log("out1");
bclick2();
};
const bclick2 = () => {
console.log("out2");
};
console.log("out4");
return (
<div className="App">
{console.log("in")}
<button onClick={bclick} />
</div>
);
}
output of console.log() after clicking on button:
out1
out2
out3
out4
in
Q> Upon clicking on button multiple different setStates are executed. Will they re-evaluate the component or the function chain(bclick and bclick2) complete executing and then App component is re-evaluated.
Based on my output I realise that function chain is executed first.
So is this how setState works?
Will flow of code complete first (irrespective of number of functions) and then functional component re-evaluated?
This has to do with React batching setState calls to optmize the number of renders. Usually you don't have to worry about that.
The setState call is async. React will decide when and how to apply multiple setState calls.
The handlers will always finish running before React re-renders. That's why you are seeing the bclick2() call running before any re-renders.
I feel that React will always go for batching multiple setState calls in a single re-render. But you can see that if you wrap multiple setState calls in setTimeout, React will re-render multiple times, because there's no way of it to know how long those timeouts will take to complete. You might be calling an API, for example.
function App() {
console.log('Rendering App...');
const [x, setx] = React.useState(1);
const [x1, setx1] = React.useState(1);
const [x2, setx2] = React.useState(1);
const [x3, setx3] = React.useState(1);
const [x4, setx4] = React.useState(1);
const [x5, setx5] = React.useState(1);
const bclick = () => {
console.clear();
console.log("From bclick (batched: single render)");
setx1(x1 + 1);
setx2(x2 + 1);
setx3(x3 + 1);
setx4(x4 + 1);
setx5(x5 + 1);
setx(x + 1);
console.log("Calling bclick2");
bclick2();
};
const bclick2 = () => {
console.log("From bclick2");
};
const notBatched = () => {
console.clear();
console.log('From notBatched (multiple renders)');
setTimeout(() => setx1((prevState) => prevState+1),0);
setTimeout(() => setx1((prevState) => prevState+1),0);
setTimeout(() => setx1((prevState) => prevState+1),0);
};
return (
<div className="App">
<button onClick={bclick}>Click (will batch)</button>
<button onClick={notBatched}>Click (will not batch)</button>
</div>
);
}
ReactDOM.render(<App/>,document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
For example, if you are calling an API from a useEffect:
useEffect(() => {
setIsLoading(true); // 1
const data = await fetchAPI();
setData(data); // 2
setIsLoading(false); // 3
},[]);
In this case React wil run #1, and then, when the API call completes, it will run #2 and #3 separately (not batched). Not sure why it chooses to do it separately, because it would be safe to run them together, but I'm sure React has its own reasons for that. Probably the whole block that has been timeout'd because of the API call is flagged to shouldNotBatch somehow. I don't actually know what is the internal logic they use for this.
const fetchAPI = () => {
return new Promise((resolve) => {
setTimeout(() => resolve('DATA'),1500);
});
}
const App = () => {
console.log('Rendering App...');
const [isLoading,setIsLoading] = React.useState(false);
const [data,setData] = React.useState(null);
// I changed the `await` to `then` because SO snippets don't allow `await` in this case
React.useEffect(() => {
console.log('Running useEffect...');
setIsLoading(true); // 1
fetchAPI().then((data) => {
setData(data); // 2
setIsLoading(false); // 3
});;
},[]);
return(
<div>
<div>isLoading:{JSON.stringify(isLoading)}</div>
<div>data:{JSON.stringify(data)}</div>
</div>
);
};
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
React v18 (apr-2022)
Apparently React v18 batches call from async handlers as well.
https://vogue.globo.com/celebridade/noticia/2022/04/bbb-portugal-bruna-gomes-e-pedida-em-namoro-por-bernardo-sousa.html

React.JS: Form problem to make post caused from useEffect

I have created a form and I have noticed that when I submit data, they are not writing in the db (with error 400). So I have investigated and I have noticed that one api call that I make in useEffect is done about 5 time during the submit. (I have tried to comment this part and It works!)
I have a first part of form, in which with a select I make a choose, this value is used to make an api call (and there is the problem) to give back some data to use in the form.
return (
<AvForm model={isNew ? {} : userClientAuthorityEntity} onSubmit={saveEntity}>
<AvInput
id="client-application"
data-cy="application"
type="select"
className="form-control"
name="application"
onChange={handleChangeApp} // there i save the value applicationApp
required
value={applicationApp}
>
<option value="" key="0">
Select
</option>
{applicationListAPP ?
applicationListAPP.map(value => {
return (
<option value={value.appCod} key={value.appCod}>
{value.appDescription}
</option>
);
})
: null}
</AvInput>
</AvGroup>
<ShowRoleApp applicationRole={applicationApp} /> // so there I pass the value to make the api call
)
const ShowRoleApp = ({ applicationRole }) => {
const [profili, setProfili] = useState([]);
const [isLoading, setIsLoading] = useState(false);
if (!applicationRole) {
return <div />;
}
// I think that it the problem, because it recall GetProfili
useEffect(() => {
async function init() {
await GetProfili(applicationRole)
.then((res) => {
console.log('res ', res);
setProfili(res);
setIsLoading(true);
})
.catch((err) => console.log('err ', err));
}
init();
}, []);
return isLoading ? (
RenderProfili(profili, applicationRole)
) : (
<div className='d-flex justify-content-center'>
<div className='spinner-border text-primary' role='status'>
<span className='visually-hidden'></span>
</div>
</div>
);
};
const GetProfili = async (appCod) => {
const chiamata = 'myApi' + appCod.toString();
const res = await fetch(chiamata);
const result = res.clone().json();
return result;
};
const RenderProfili = (profili, applicationRole) => {
const ruoliOperatore = profili ? profili.filter(it => it.appCod.toString() === applicationRole.toString()) : null;
return (
<AvGroup>
<Label for="sce-profiloutentepa-pucCod">Profile (*)</Label>
// other code for the form...
So in your opinion how can i do to call the GetProfili without recall every time when I submit the form?
Thank you
You could define GetProfili as a custom hook an manage the useEffect call in it.
It will return the isLoading and profili instances.
Try to change your code like this.
GetProfili:
const GetProfili = (appCod) => {
const [isLoading, setIsLoading] = useState(true)
const [profili, setProfili] = useState([])
const loadProfili = async () => {
const chiamata = 'myApi' + appCod.toString();
const res = await fetch(chiamata);
setProfili(res.json())
setIsLoading(false)
}
useEffect(() => {
loadProfili()
}, [])
return { isLoading, profili };
};
ShowRoleApp:
const ShowRoleApp = ({ applicationRole }) => {
if (!applicationRole) {
return <div />;
}
const { isLoading, profili } = GetProfili(applicationRole)
return isLoading ? (
RenderProfili(profili, applicationRole)
) : (
<div className='d-flex justify-content-center'>
<div className='spinner-border text-primary' role='status'>
<span className='visually-hidden'></span>
</div>
</div>
);
};
I didn't really understand the question but I can say something that might help. The useEffect() hook gets called on every rerender of the component so if it updates 5 times its because some states inside the component get updated 5 times. Also states are updated in child components update the parent.

Run one function after another

I am trying to to set a hook on click. When that hook is set, it enters a url and then when it is set, it is supposed to run a handleSubmit function to update the urls and display it to screen. My problem is that the function run at the same time. I have tried to use the useEffect method, by placing the handleSubmit function in there, but it keeps giving errors about the event object. I have tried the async/await function on the onClick method but have read that it doesn't work on hooks. I have read the promises docs but they are confusing right now. Can anyone point me in the right direction?
const Peers = ({ peerData, symbol, handleSubmit }) => {
const [peerSymbol, setPeerSymbol] = useState('');
let today = new Date().toISOString().slice(0, 10)
const urls = [
`https://finnhub.io/api/v1/company-news?symbol=${peerSymbol}&from=2021-03-01&to=${today}&token=`,
`https://finnhub.io/api/v1/stock/peers?symbol=${peerSymbol}&token=`,
`https://finnhub.io/api/v1/stock/profile2?symbol=${peerSymbol}&token=`,
`https://finnhub.io/api/v1/stock/financials-reported?symbol=${peerSymbol}&token=`,
`http://api.marketstack.com/v1/tickers/${peerSymbol}/eod/latest?access_key`
]
useEffect(() => {
let e = e
return (e) => handleSubmit(e, urls);
}, [peerSymbol])
return (
<div className="peers bg-light">
<h2>Peers</h2>
{peerData.filter(peer => {
return peer !== symbol.toUpperCase();
}).map(element => {
return <span
key={element}
onClick={async (e) => { setPeerSymbol(element); handleSubmit(e, urls) }}>{element}</span>
})}
</div>
);
}
Add a function outside the component's body as getUrls and call it with the element and date:
const getUrls = (peerSymbol, today) => ([
`https://finnhub.io/api/v1/company-news?symbol=${peerSymbol}&from=2021-03-01&to=${today}&token=budo2rv48v6spq9og4p0`,
`https://finnhub.io/api/v1/stock/peers?symbol=${peerSymbol}&token=budo2rv48v6spq9og4p0`,
`https://finnhub.io/api/v1/stock/profile2?symbol=${peerSymbol}&token=budo2rv48v6spq9og4p0`,
`https://finnhub.io/api/v1/stock/financials-reported?symbol=${peerSymbol}&token=budo2rv48v6spq9og4p0`,
`http://api.marketstack.com/v1/tickers/${peerSymbol}/eod/latest?access_key=72d118ca9db1873033447561590e2794`
]);
const Peers = ({ peerData, symbol, handleSubmit }) => {
const [peerSymbol, setPeerSymbol] = useState('');
const today = new Date().toISOString().slice(0, 10)
return (
<div className="peers bg-light">
<h2>Peers</h2>
{peerData.filter(peer => {
return peer !== symbol.toUpperCase();
}).map(element => {
return <span
key={element}
onClick={async (e) => { setPeerSymbol(element); handleSubmit(e, getUrls(element, today)) }}>{element}</span>
})}
</div>
);
}
this way you don't have to rely on the component's state to update before calling handleSubmit and you can remove useState if it's no longer needed.

React hooks: How to read & update state in hooks without infinite loops with react-hooks/exhaustive-deps rule

When state is in a hook it can become stale and leak memory:
function App() {
const [greeting, setGreeting] = useState("hello");
const cb = useCallback(() => {
alert("greeting is " + greeting);
}, []);
return (
<div className="App">
<button onClick={() => cb()}>Click me</button>
<p>
Click the button above, and now update the greeting by clicking the one
below:
</p>
<button onClick={() => setGreeting("bye")}>
Update greeting
</button>
<p>Greeting is: {greeting}</p>
<p>
Now click the first button again and see that the callback still has the
old state.
</p>
</div>
);
}
Demo: https://codesandbox.io/s/react-hook-stale-datamem-leak-demo-9pchk
The problem with that is that we will run into infinite loops in a typical scenario to fetch some data if we follow Facebook's advice to list all dependencies always, as well as ensure we don't have stale data or memory leaks (as the example showed above):
const [state, setState] = useState({
number: 0
});
const fetchRandomNumber = useCallback(async () => {
if (state.number !== 5) {
const res = await fetch('randomNumber');
setState(v => ({ ...v, number: res.number }));
}
}, [setState, state.number]);
useEffect(() => {
fetchRandomNumber();
}, [fetchRandomNumber]);
Since Facebook say we should list fetchRandomNumber as a dependency (react-hooks/exhaustive-deps ESLint rule) we have to use useCallback to maintain a reference, but it regenerates on every call since it both depends on state.number and also updates it.
This is a contrived example but I've run into this many times when fetching data. Is there a workaround for this or is Facebook wrong in this situation?
Use the functional form of the state setter:
const fetchData = useCallback(async () => {
const res = await fetch(`url?page=${page}`);
setData((data) => ([...data, ...res.data]));
setPage((page) => page + 1);
}, [setData, setPage]);
Now you don't need data and page as your deps
You can also use a ref to run the effect only on mount :
const mounted = useRef(false);
useEffect(() => {
if(!mounted.current) {
fetchSomething();
mounted.current = true;
}
return () => { mounted.current = false }
}, [fetchSomething]);
And
const fetchSomething = useCallback(async () => {
...
}, [setData, setPage, data, page]);
fetchSomething is not a dependency here. You don't want to retrigger the effect, you only cause it once when the component mounts. Thats what useEffect(() => ..., []) is for.

Categories