Using (async) generators for reporting progress

Photo by Eugene Zhyvchik on Unsplash

Context

In our product, we allow users to login using 3rd party applications that we have integrated with, and for this we have a multi step async login process.

  • the 3rd party redirects to our frontend with a token in the url, the FE extracts that token, makes an async api call to get some other relevant info based on this extracted token
  • The FE then sends the info to backend and the backend in response sets the user JWT. This in itself requires multiple async calls from FE to BE
  • Post this, the FE needs to make some more api calls to get some important user details, set them in global state, and more, before displaying the UI.

As you would have guessed, this is bound to take some time, and in some cases there may be failures at certain steps in the process.

To make sure that we are able to provide the users a good experience while they wait, we have implemented a LoginLoader component, which shows the progress while all this is happening behind the scenes, and shows an Error UI in case an error happens during any of the steps.

For this component to know the progress, it needs to somehow subscribe to when the sequential but async api calls happen. In our current implementation, there is a login method which abstracts most of this complexity, and it takes an onProgress callback, which it calls during different stages of the login process.

Issues with the current implementation

The current implementation of the login function looks similar to this:

function login(onProgress, ...) {
try {
const step1Result = await step1();
onProgress(0.3);
const step2Result = await step2(step1Result);
onProgress(0.65);
const step3Result = await step3(step2Result);
onProgress(1);
} catch(e) {
...
}
...
}

The login function internally uses promises / async await, but the calling function needs to pass in a callback for onProgress. This isn't a major problem on its own, specially if you own both the calling function and the called function.

One of the major inconveniences I had with this setup in our case, was that the onProgress function was being passed from a react component - LoginLoader, which was setting some internal component state in this function. This is slightly unsettling for reasons that I can't pinpoint at this point, but this just feels impure / dirty / clunky. It is like passing the setState function to a service / util which lives outside the component. The service / util itself can be tested in isolation, and is "pure" from an understanding perspective, but the component has now given control to something outside itself to directly change its state. It would be much more easier to reason about of somehow the progress can be returned as a value by calling the login function, so that the component could consume it as a return value from a regular function. (Let me know in the comments if you have some concrete thoughts around why this feels clunky)

const LoginLoader = (props) => {
const [progress, setProgress] = useState(0);
...
useEffect(() => {
...
login(setProgress);
...
}, [...]
)
...
return (
...
<div
styleName="progress-bar-inner"
style={{ transform: `scaleX(${props.progress})` }}
/>
...
)
}

Another problem with this implementation is that it suffers from the same drawbacks as other callback based functions, the major one being that you don’t have control over whether your callback would be called, or if it would be called at the correct time, or if it would be called too many times, if any future changes in the login function change the implementation. As discussed, this is less of a concern here, as we own both the functions, so it would be possible for us to make sure that any new change in login function does not break the existing usage.

Another smaller consideration is that the caller has no way to ask the login function to pause before moving to the next step. This would be helpful in cases where the caller needs to take some user input before allowing the user to move ahead with the login, or when the caller wants to add some fake delay in a certain step for user experience before moving to the next step.

A better implementation

Reiterating our concerns:

  • make the component in control of setting its own state instead of passing it to a function outside the component.
  • Somehow promis-ify the setup so that we can consume the return value similar to a regular (async) function call
  • cease control to the caller multiple times (every time we report progress), so that they can control moving to the next step.
  • the login function implementation should not depend on how the consumers want to consume the progress, now or in future, and its only responsibility should be to expose the progress.

All of these problems can be individually or collectively solved with callbacks or promises. I have always used promises to replace callbacks, and to provide more control to the caller. In this case, we need to cease control to the caller multiple times (every time we report progress), so there is no (easy) way to promis-ify this setup. One pattern that fits very naturally with this is generators. we need to return multiple values and we need to cease control to the caller. We need to await multiple functions from inside the generator so we would need an async generator.

This is how the new implementation looks like:

async function* login() {
try {
const step1 = await stepOne();
yield 0.3;
const step2 = await stepTwo();
yield 0.65;
const step3 = await stepThree();
yield 1;
} catch (e) {
console.log(e);
}
}

A quick thing to note is that we do not need to yield the actual login related promises from inside the generator, just the progress values. The consumer does not want to directly wait on the async calls that happen during login, but just be notified about the completion and get the progress value.

This solves all of the problems that we had with the callback implementation:

  • the component does not need to pass a onProgress callback anymore, so it does not need to expose the state setter to something outside of itself.
  • the caller can control when to pause and resume the steps, and hence can add fake delays or take user inputs in between the steps.
  • Once implemented like this, the login function does not need to care about any future requirements that it’s consumers might have for impacting the flow (pause, add delay, etc)

References

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Java Script and JSON for newbies

A Solid Webpack Config and the Knowledge Behind It

JSX Rules in React(A JavaScript Framework):

React + Redux + JWT

Configure Redux with React

Rebuilding with Eleventy

How To Clone An Array In JavaScript

Programmer.

A review of tech fundamentals 1 of 2

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Gaurav Gupta

Gaurav Gupta

More from Medium

How to managed states globaly with context?

Tips on Creating Unit Tests Using Jest

How to use Context-API in React

Typescript: A guide for faster onboarding process