How to display progress to user during long running process in React - reactjs

I am building an app where a user provides a file and some parameters to then perform a long running task. I have all of that working. What is not working is showing the user the current progress of processing. I have a simple CodePen set up to illustrate.
In the Pen, I have a button that runs a task in a while loop. If I am looking at the console, I can see the progress printing out as we step through the loop. However, the state isn't updating until the loop is done, so in the UI the progress jumps from 0 to 5 without displaying the intermediate values. Here I am simulating the task with the sleep function, I do not actually use the sleep function in my app.
I've done some research and I know this has to do with setState being asynchronous and with React batching updates together to be more efficient with rendering the UI.
With that being said, I am wondering what the best way to display progress to the user would be. Using React's state doesn't work, and I've tried directly writing to the DOM but that wasn't working (and it didn't seem like a clean way to do it). Do I need to use some additional library to do this or is there something I am missing? I was considering moving this to a separate process and then communicating the progress back to the app, but wouldn't I run into the same issue of the UI not updating?
Also potentially important, I am using the while loop because I am using a generator, so I know I won't receive too many progress updates because yield runs each percentage point from 0 to 100. It is also easy for me to remove the generator/yield part if that would be better.
My code is in CodePen as well as below:
---HTML---
<div id="app"></app>
---JSX---
class Application extends React.Component {
constructor(props) {
super(props);
this.state = {
progress: 0
};
this.doTask = this.doTask.bind(this);
this.sleep = this.sleep.bind(this);
}
sleep(milliseconds) {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}
doTask() {
let count = 0;
while(count<5) {
count++;
console.log(count);
this.setState({
progress: count
});
this.sleep(500);
}
}
render() {
return <div>
<button onClick={this.doTask}>Do Task</button>
<div>Progress: {this.state.progress}</div>
</div>;
}
}
/*
* Render the above component into the div#app
*/
React.render(<Application />, document.getElementById('app'));

This is a recurring issue in development, so I hope this question and solutions helps you out: Calling setState in a loop only updates state 1 time.
I would also take a look at https://jsbin.com/kiyaco/edit?js,output where an alternate form of this.setState is used. It essentially passed in a function which has access to the current state.

What I ended up doing was creating a background process (aka a hidden window because I'm using electron) that basically acts as a server. The information for my long running process is sent to the background process via a websocket and the progress information is sent back to my main component also via a websocket. Updating the state from a loop seems to me to be more intuitive, but running my code in the background doesn't freeze up the UI and now I get my desired behavior.

I have the same issue and used a workaround. For some reason, when using setTimeout() the page does the setState() processing correctly. It remembered me of runLater in JavaFX.
So this is a workaround I used:
// a wrapper function for setState which uses `Promise` and calls `setTimeout()`
setStatePromise = (state) => new Promise(resolve => {
this.setState(state, () => {
setTimeout(() => resolve(), 1);
});
});
Now you can call this wrapper instead. But you'll need to use async functions:
async doTask() {
let count = 0;
while(count<5) {
count++;
console.log(count);
await this.setStatePromise({
progress: count
});
this.sleep(500);
}
}

Related

After sending each message there becomes 2 more messages using socket io

I have a React website.
I receive messages like this:
useEffect(() => {
socket.on('message', message => {
console.log(message)
})
}, [socket])
I send messages like this:
socket.emit('chatMessage', { message, id })
Server side:
socket.on('chatMessage', ({ message }) => {
socket.broadcast.emit('message', message)
})
First time there is 2 message (1 for the user who sent it), the next time there is 4, 6, 8 and so on.
Cleaning up the connections from the previous renders
useEffect(() => {
let isValidScope = true;
socket.on('message', message => {
console.log(message)
// if message received when component unmounts
// stop executing the code
if (!isValidScope) { return; };
// if you need to access latest state, props or variables
// without including them in the depedency array
// i.e you want to refer the variables without reseting the connection
// use useRef or some custom solution (link below)
})
return () => {
// cleanup code, disconnect
// socket.disconnect()
isValidScope = false;
}
}, [socket])
more about useEffect life cycle, to get an idea why
A new effect is created after every render
How the cleanup for previous effect occurs before executing of current useEffect
You can read about why isValid is set synchronizing with effects
Why it was running 3 times in dev mode
If you are intererested in taking a deep dive, consider reading a blog post by Dan on useEffect, its old but helps to build a good mental model about useEffects and functional components.
useEvent can solve the problem but it is in RFC
you can check my question about a implementation to build a custom useEvent till it becomes stable
Hope it helps, cheers
The problem with your code is you assume your component will never be recreated. But React does not provide such guarantees. And if you will add logging at the place when you open a socket, you will notice that for the first render it will be called 2 times. And because you do not have cleanup code the socket remains open even after the component is destroyed. Thus duplicated messages.
Furthermore it would seem that your component is recreated on every message, which multiplies the existing effect of duplication.
The solution in your case would be to close the connection in cleanup part of the effect.

Handling OAuth with React 18 useEffect hook running twice

Background
I have recently upgraded a fairly sizeable React app to React 18 and for the most part it has been great. One of the key changes is the new double mount in development causing useEffect hooks to all run twice, this is clearly documented in their docs.
I have read their new effect documentation https://beta.reactjs.org/learn/lifecycle-of-reactive-effects and although it is quite detailed there is a use case I believe I have found which is not very well covered.
The issue
Essentially the issue I have run into is I am implementing OAuth integration with a third-party product. The flow:
-> User clicks create integration -> Redirect to product login -> Gets redirected back to our app with authorisation code -> We hit our API to finalise the integration (HTTP POST request)
The problem comes now that the useEffect hook runs twice it means that we would hit this last POST request twice, first one would succeed and the second would fail because the integration is already setup.
This is not potentially a major issue but the user would see an error message even though the request worked and just feels like a bad pattern.
Considered solutions
Refactoring to use a button
I could potentially get the user to click a button on the redirect URL after they have logged into the third-party product. This would work and seems to be what the React guides recommend (Although different use case they suggested - https://beta.reactjs.org/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers).
The problem with this is that the user has already clicked a button to create the integration so it feels like a worse user experience.
Ignore the duplicate API call
This issue is only a problem in development however it is still a bit annoying and feels like an issue I want to explore further
Code setup
I have simplified the code for this example but hopefully this gives a rough idea of how the intended code is meant to function.
const IntegrationRedirect: React.FC = () => {
const navigate = useNavigate();
const organisationIntegrationsService = useOrganisationIntegrationsService();
// Make call on the mount of this component
useEffect(() => {
// Call the method
handleCreateIntegration();
}, []);
const handleCreateIntegration = async (): Promise<void> => {
// Setup request
const request: ICreateIntegration = {
authorisationCode: ''
};
try {
// Make service call
const setupIntegrationResponse = await organisationIntegrationsService.createIntegration(request);
// Handle error
if (setupIntegrationResponse.data.errors) {
throw 'Failed to setup integrations';
}
// Navigate away on success
routes.organisation.integrations.navigate(navigate);
}
catch (error) {
// Handle error
}
};
return ();
};
What I am after
I am after suggestions based on the React 18 changes that would handle this situation, I feel that although this is a little specific/niche it is still a viable use case. It would be good to have a clean way to handle this as OAuth integration is quite a common flow for integration between products.
You can use the useRef() together with useEffect() for a workaround
const effectRan = useRef(false)
useEffect(() => {
if (effectRan.current === false) {
// do the async data fetch here
handleCreateIntegration();
}
//cleanup function
return () => {
effectRan.current = true // this will be set to true on the initial unmount
}
}, []);
This is a workaround suggested by Dave Gray on his youtube channel https://www.youtube.com/watch?v=81faZzp18NM

React log final state value after delay

I have a MERN app. On the react side, I have a state. This state may or may not change many times a second. When the state is updated, I want to send the state to my back end server API so I can save the value in my mongodb. This state can possibly change hundreds of times a second which I wish to allow. However, I only want to send this value to my server once every 5 seconds at most. This is to avoid spam and clogging my mongodb Atlas requests.
Currently, I have tried setInterval, setTimeout and even locking cpu with a while(time<endTime).
These have all posed an issue:
The setInterval is nice since I could check the currentValue with the lastSentValue and if they do not equal (!==) then I would send the currentValue to my server. Unfortunately, when I set interval, it returns the initial value that was present when the setInterval was called.
If you know how I can let a user spam a boolean button while only sending updates at most once every 5 seconds from the front end (React) to the back end (Node) and that it sends the current and up to date value then please share your thoughts and I will test them as soon as possible.
My state value is stored as::
const [aValue, anUpdate] = useState(false);
The state is changed with an onClick method returned in my React app.
function Clicked(){
anUpdate(!aValue);
}
My set interval test looked like this::
//This is so that the button may be pressed multiple times but the value is only sent once.
const [sent, sentUpdate] = useState(false);
//inside of the clicked method
if(!sent){
sentUpdate(true);
setInterval(()=>{
console.log(aValue);
},1000);
}
My setTimeout is very similar except I add one more sentUpdate and reset it to false after aValue has been logged, that way I can log the timeout again.
//setInterval and setTimeout expected results in psudocode
aValue=true
first click->set aValue to !aValue (now aValue=false), start timeout/interval, stop setting timeouts/interval until completed
second click->set aValue to !aValue (now aValue=true), do not set timeout/interval as it is still currently waiting.
Completed timeout/interval
Log->false
//expected true as that is the current value of aValue. If logged outside of this timeout then I would receive a true value logged
In quite the opposite direction, another popular stackOverflow answer that I stumbled upon was to define a function that used a while loop to occupy computer time in order to fake the setTimeout/setInterval.
it looked like this::
function wait(ms){
let start = new Date().getTime();
let end = start;
while(end < start + ms) {
end = new Date().getTime();
}
}
Unfortunately, when used inside of the aforementioned if statement (to avoid spam presses) my results were::
aValue=true
first click->set aValue to !aValue (now aValue=false), start wait(5000), turn off if statement so we don't call many while loops
second click->nothing happens yet - waiting for first click to end.
first click timeout->logged "false"->if statement turned back on
second click that was waiting in que is called now->set aValue to !aValue (now aValue=true), start wait(5000), turn off if statement so we don't call many while loops
second click timeout->logged "true"->if statement turned back on
So the while loop method is also not an option as it will still send every button press. It will just bog down the client when they spam click.
One more method that I saw was to use a Promise to wrap my setTimeout and setInterval however that in no way changed the original output of setTimeout/setInterval.
It looked like this::
const promise = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(true);
},5000);
});
promise.then(console.log(aValue));
//I also tried resolve(aValue)->promise.then(val=>console.log(val));
For what you are trying to do with looping and starting intervals in the callback will only ever close over a specific state value fo reach iteration, i.e. the initial state value.
The solution is to use an useEffect hook to handle or "listen" for changes to a dependency value. Each time the state updates a component rerender is triggered and the useEffect hook is called, and since the dependency updated, the hook's callback is called.
useEffect(() => {
sendStateToBackend(state);
}, [state]);
If you want to limit how often sendStateToBackend is actually invoked then you want to throttle the call. Here's an example using lodash's throttle Higher Order Function.
import { throttle } from 'lodash';
const sendStateToBackend = throttle((value) => {
// logic to send to backend.
}, 5000);
const MyComponent = () => {
...
useEffect(() => {
sendStateToBackend(state);
}, [state]);
...
Update
If you want to wait until the button is clicked to start sending updates to the backend then you can use a React ref to track when the button is initially clicked in order to trigger sending data to backend.
const clickedRef = React.useRef(false);
const [aValue, anUpdate] = React.useState(false);
React.useEffect(() => {
if (clickedRef.current) {
sendToBackend(aValue);
}
}, [aValue]);
const clicked = () => {
clickedRef.current = true
anUpdate(t => !t);
};
...
See also Lodash per method packages since package/bundle size seems a concern.
So I had a brain blast last night and I solved it by creating a series of timeouts that cancel the previous timeout on button click and set a new timeout with the remaining value from the last timeout.
const [buttonValue, buttonUpdate] = useState(props.buttonValue);
const [lastButtonValue, lastButtonUpdate] = useState(props.buttonValue);
const [newDateNeededValue, newDateNeededUpdate] = useState(true);
const [dateValue, dateUpdate] = useState();
const [timeoutValue, timeoutUpdate] = useState();
function ButtonClicked(){
let oldDate = new Date().getTime();
if(newDateNeededValue){
newDateNeededUpdate(false);
dateUpdate(oldDate);
}else{
oldDate = dateValue;
}
//clear old timeout
clearTimeout(timeoutValue);
//check if value has changed -- value has not changed, do not set timeout
if(lastButtonValue === !buttonValue){
console.log("same value do not set new timout");
buttonUpdate(!buttonValue);
return;
}
//set timeout
timeoutUpdate(setTimeout(()=>{
console.log("timed out");
lastButtonUpdate(!buttonValue);
newDateNeededUpdate(true);
//This is where I send to my previous file and send to my server with axios
props.onButtonUpdate({newVal:!buttonValue});
clearTimeout(timeoutValue);
}, (5000-(new Date().getTime() - oldDate))));
}

How can I get progress of a long task out of a function in my react/redux app?

I'm currently trying to come up with a way of getting information out of a long running task (few seconds to a minute long) that occurs after uploading a file in my app. I am using react/redux and am currently trying this:
export const longTask = (file) => (dispatch) => {
// setup file ready
fileReader.onload = () => {
let lastTime = Date.now();
const lines = fileReader.result.split('\n');
for (let i = 0; i < lines.length; i++) {
const currentTime = Date.now();
if (currentTime - lastTime >= 100) {
dispatch(updateNumber(i / lines.length));
lastTime = currentTime;
}
// Do long, intensive task for current line
}
}
}
which would operate inside of redux-thunk. The problem is though, that dispatch only queues the actions to run, it doesn't run them immediately and that this blocks the rendering of React as a whole. Does anyone have a pattern that would help with this sort of work? I don't really want to offload the work to a web-worker, since the work is constructing a data object that is quite large and transfering data to and from a web-worker is through value and not reference (implies data copying).
Edit: Note that I am using React v15.6.2, not v16.*, since it conflicts with Golden Layout.
I think if the problem is what you described you could rewrite the long task so that after some number of iterations you schedule the rest of the work e.g. with setTimeout.
My approach would be to have a reducer that has a state.longTask.queue array in which you put all the lines, and then have a react component dispatch an action that only processes the next line in the queue. This would give control back to react where you can update the progress accordingly and you get a render cycle in between two lines being processed. No need for setTimeout..

share() vs ReplaySubject: Which one, and neither works

I'm trying to implement short-term caching in my Angular service -- a bunch of sub-components get created in rapid succession, and each one has an HTTP call. I want to cache them while the page is loading, but not forever.
I've tried the following two methods, neither of which have worked. In both cases, the HTTP URL is hit once for each instance of the component that is created; I want to avoid that -- ideally, the URL would be hit once when the grid is created, then the cache expires and the next time I need to create the component it hits the URL all over again. I pulled both techniques from other threads on StackOverflow.
share() (in service)
getData(id: number): Observable<MyClass[]> {
return this._http.get(this.URL)
.map((response: Response) => <MyClass[]>response.json())
.share();
}
ReplaySubject (in service)
private replaySubject = new ReplaySubject(1, 10000);
getData(id: number): Observable<MyClass[]> {
if (this.replaySubject.observers.length) {
return this.replaySubject;
} else {
return this._http.get(this.URL)
.map((response: Response) => {
let data = <MyClass[]>response.json();
this.replaySubject.next(data);
return data;
});
}
}
Caller (in component)
ngOnInit() {
this.myService.getData(this.id)
.subscribe((resultData: MyClass[]) => {
this.data = resultData;
},
(error: any) => {
alert(error);
});
}
There's really no need to hit the URL each time the component is created -- they return the same data, and in a grid of rows that contain the component, the data will be the same. I could call it once when the grid itself is created, and pass that data into the component. But I want to avoid that, for two reasons: first, the component should be relatively self-sufficient. If I use the component elsewhere, I don't want to the parent component to have to cache data there, too. Second, I want to find a short-term caching pattern that can be applied elsewhere in the application. I'm not the only person working on this, and I want to keep the code clean.
Most importantly, if you want to make something persistent even when creating/destroying Angular components it can't be created in that component but in a service that is shared among your components.
Regarding RxJS, you usually don't have to use ReplaySubject directly and use just publishReplay(1, 10000)->refCount() instead.
The share() operator is just a shorthand for publish()->refCount() that uses Subject internally which means it doesn't replay cached values.

Resources