Asynchronous UX

Rate this content

"Please do not close or leave this page" may send shivers down your spine, but coding the proper UX flow for async might make you question your daily job. How can we properly handle UX for asynchronous code in highly responsive applications? Let's explore how introducing asynchronous code creates a challenge for UX.

21 min
25 Oct, 2021


Sign in or register to post your comment.

Video Summary and Transcription

Today's Talk covers the importance of building Asynchronous UX with React and single-page applications, providing code and UX examples. It explores data fetching, adding progress indicators, handling errors, and user-initiated actions. The Talk also discusses handling component unmounts, multiple actions, idempotency, and context loss. Finally, it touches on considerations for optimistic updates and the use of CRDT or other technologies for collaborative applications.

Available in Español

1. Introduction to Asynchronous UX

Short description:

Today, I'll be talking about Asynchronous UX with React and single-page applications. I'll cover scenarios you'll encounter in your daily job and provide code examples and UX examples. The key takeaway is the importance of building this kind of UX for users and the development cost.

Hi, thank you for joining today's session. My name is Tony and today I'll be talking to you about Asynchronous UX with React and single-page applications. When we say that the web is asynchronous, we are referring to the underlying request-response nature of the web, where a response from the server might take some time to arrive. Traditionally, applications built with server-side rendering in mind were handled by the browser. When we build the same application as a single-page application, we have to build new affordances for the user because the browser can't handle them for us. In today's session, I'm going to go over certain scenarios you will encounter in your daily job and try to address them with code examples and UX examples. At the end of the talk, I hope you'll take away from it that it's very important to build this kind of UX for the users and how much it will cost in terms of development.

2. Exploring Data Fetching and UX

Short description:

Before we begin, let's divide this topic into two areas: read-only data patches and user-initiated actions that change the server state. When navigating the page, fetching data may take some time. To improve the UX, we can add a progress bar or employ skeleton UX. Building more consistent UX can be complex, especially when multiple components have their own proxy indicators. To avoid flickering effects, we can introduce a delay before showing the progress indicator. Let's look at a naive example of code for a page and explore how it can be improved.

Before we begin, I just want to divide this whole topic in two different areas. The first area deals with read-only data patches, and the second area is user-initiated actions that change the state of the server, basically doing form submissions while the queries are page navigations.

Let's talk about queries. So whenever you navigate the page, you might fetch some data. In this naive example, when we navigate to a page, we will fetch the data and then display it to the user. This is a happy scenario where the data arrives somewhat quickly, but depending on the network load, the server load, or the user data, waiting for the data to arrive might take some time. While the user is waiting, we kind of want to inform them that they have to wait in case there are no results. We want to tell them there are no results. If there's an error, we want to tell them that there was an error and what they can do about it.

To improve this UX a bit, we want to add a simple progress bar. This progress is indeterminate because we don't know when it will end, and the user might just want to stick around and wait for the data to arrive. To improve on this very simple progress indicator, we might want to employ skeleton UX. Now skeleton UX looks like the data is going to replace. Basically, this stops flickering effect from happening, because the users will see the outline of the incoming data, and then when the data arrives, it basically just replaces hopefully in place. Unlike progress indicator, skeleton UX is much, much harder to build and can quickly go out of sync with the components they're trying to mimic. So be wary of the complexities involved in building them.

For a bit more complicated page, you can see that when the data is there, you might want to have partial updates with fetching more data. You might want to have different ways of fetching data which might yield different data sizes. So in this case, we can see that sometimes you get little data, sometimes you get no data, sometimes you get an error. And when you have data fetching, you kind of want to reuse this UX. As you can see, building more consistent UX requires some explaining, and you can imagine how much more complicated the code might be. When we build components in our applications, we might want to focus too much on a single component, so each component has its own data fetching and its own proxy indicator. What can happen is that we can quickly end up in a situation where multiple components on the same screen have their own proxy indicators, and then we have this flickering effect as different components have data arriving in a different order. Also, when we have very fast servers, it can be annoying because of the flickering effect because data will arrive very quickly and then the users will see the progress indicator for just a brief time. So to improve that experience, we might want to introduce some kind of delay before showing the progress indicator, so only showing the progress indicator when the data takes a long time to arrive.

Let's take a look at a very naive example of how the code would look for a page. Regardless of whether or not you use React query or Apollo or something similar, your component might look like something like this – you fetch data and you display it inside your UI. Now, can we do better than this? Of course we can, but it will require additional work and it will require a different type of work. So let's take a look at how much more complicated the code can get. On the left side, we can see the naive example from before, and on the right side we'll keep improving it by handling different scenarios that we've shown in the previous demos.

3. Adding Progress Indicators and Handling Errors

Short description:

The first thing to add is the progress indicator. It can be implemented as a top-level loading indicator or inline below the H1. Handling errors, empty states, and skeleton loaders can greatly improve the UX. Introducing a delay and preventing the display of multiple progress indicators are important. Handling backend errors depends on the application, including user fixable errors, server load issues, general failures, and network connection problems. Improving the UX for long running responses is also crucial.

So the first thing we want to add is the progress indicator. Now, that is kind of straightforward, we just have some kind of flag saying that, yeah, loading is being done, in which case we show the loading UI. Now, you can already see that this can be done in two different ways, either as a top-level loading indicator or you can actually inline below the H1 and thus have a bit more intrusive loading indicator. Your opinions influence the way you implement this. If we want to handle errors, we have to add yet additional code, and you can see that this displaying of the error might also change location and be a bit more intrusive, thus making this pattern harder to extract in a higher-order component.

You can also handle, for example, the empty state, which, as you can see, it goes a bit more intrusive in the overall structure of the page. So as you can see, adding support for many UX scenarios will have a different impact on your code. And sometimes it will be hard to squint and see how we can actually isolate this in a higher-order component that can be reusable across the application. So there is always more that we can add to the page that can improve the UX. So empty states generally improve situations where the application is brand new for the user, basically they have no data, or when they are, for example, searching for stuff that doesn't exist. So in this case, we want to actually always show the results to the user, or tell them that there are no results. That improves the UX tremendously.

Skeleton loaders are a much better UX over the regular, simple progress indicators, but they require a bit more development time, they can quickly go out of sync with a target UI that they will replace. So take special care about introducing skeleton UIs because of the complexities they bring. Progress indicators, again, just a naive boolean, don't show, might not be enough. So we want to introduce some kind of delay to prevent the flickering effect. And also we want to prevent this forest of progress indicators, and only keep the indicators for something that's really, really slow. We haven't really talked much about how to handle backend errors, because it depends on the application. There are different kinds of errors. There are the user fixable errors, basically they can do something else. There is the retry later because the server has a tremendous load. So we can't really handle the request right now. Or there can be a general failure where we redirect the user to use the support, and basically asks us what went wrong. There's also a special case about network connection issues. For example, if you're in high density urban area or going out of reach, like in a tunnel or just in a rural part of the country, you might actually drop connection or have this intermittent connection issues. In that case the UI must or should handle these cases by showing to the user that right now data cannot be fetched and they should either refresh the full page or display a button where they can retry the fetch themselves when the connection recovers. You also might want to improve the UX for the long running responses. Sometimes waiting for more than five seconds will just look bad for the user. They will not know what to do. They might want to refresh the whole page.

4. Handling User-Initiated Actions and Side Effects

Short description:

They don't know what went wrong. In the side effectful area, we want to handle user-initiated actions and inform the user about the action's progress and any issues. We also need to consider side effects in the application. Let's look at a demo of a responsive UI where a button click triggers an action and provides feedback. However, double clicking can cause issues, and we need to handle them properly.

They don't know what went wrong. So you might want to actually change the text message after a certain while or do different kinds of improvements.

If we take a look at the second area, the side effectful area, we want to see how to handle the user initiated actions. Now, because the user initiated an action, unlike navigating, they want to know that the action is being handled. They want to know if the action has been done handling, are there any issues. In case of an issue, is there anything the user can do or is just a general failure.

We also want to take a look are there any side effects in that application because depending on what the user did, it can change. So let's again take a look at some demos. In this case, we're going to take a look at a very simple attempt at building a responsive UI. So when you click a button, after a while you get a success message saying the action has been completed and the user can continue doing whatever they were doing. However, you can see that this is a really sad experience because the user can double click and then we don't know what the server side can do in that case. We might create double entries or something similar.

5. Handling Errors and Idempotency

Short description:

In the case of an error, you never know what the error action is. A well-behaving button disables itself to prevent double submission, displays a progress indicator, and informs the user when the action is done. To improve user experience, we can implement an idempotency key to prevent duplicate actions. Single-page applications may cause side effects if the user navigates away before an action completes.

In the case of an error, you never know what the error action is. Did we misconfigure click handler? Was a network drop? Did the server respond with a 500? We don't know. And we want to inform our users that there's been something wrong. A well-behaving button, in this case, disables itself so that it prevents double submission. It also displays a progress indicator. And also when it's done, it informs us that something has happened. This is the example we want to strive for.

Some applications, when you're doing very advanced things, might want to tell you that please don't leave this page. Please wait until the operation is done. This might take some time. You never know how much. In a happy pet scenario, this will take a second, and you're done. However, if this page stays on and on, the user might be confused or worried. And they're not really sure if they should leave the page, what happens if they refresh. And that can yield subpar user experiences.

Now, because this is an example of booking tickets known to everyone, we want to see what we can do to improve the user experience. So let's take a look at this button. Let's say it takes a long time to execute and during this time something's happening in the server. After a while, the result comes back and we book it. Now I'm going to do something different. I'm going to click on a button and refresh the page. This is just basically destroying the context. When we go back to the page and we click the same button, it's done almost immediately because the server-side response, yes, the operation you requested was actually done before and this is the result I would have sent the previous time.

How would we do something like this? The idea is very simple, a bit more hard to execute for such important actions that really should not happen twice. We implement something called idempotency key. Basically, we generate a random key, assign it to the action and then we remember this key in local storage and when sending the request to the server, we send the same key. On subsequent requests, the server will duplicate the request and actually respond with the previous response, thus not doing the same operation twice. This is very important if you want to handle, for example, payments or any kind of limited resources in the real world because you don't want to do it twice. You don't want to order two things or pay for the same thing twice.

Now, because single-page applications are not built the same way as server-side renders, what can happen is that when you perform an action on one screen and if it takes too long the user can leave and go to another page and then when the action completes, the response will come and side effects might happen that will potentially cause bugs.

6. Handling Component Unmounts and Side Effects

Short description:

When we click on a button and then leave to a different page, we may still see the same alert. However, if we try to modify React UI, we'll get a warning about a memory leak. To handle this, we need to remember when a component unmounts and avoid performing side effects.

So, let's take a look at this example. When we click on this button, an action done will be said that this was completed. If we click and then leave to a different page because nothing prevents us from leaving, we can still see that we have the same alert. Now, this is just illustrative. If we try to do something like set state or try to modify React UI, we'll actually get the warning in the console saying that we have a memory leak. We should not do anything because the component has been unmounted. To properly handle this, we need to kind of remember that when component unmounts, we actually wanna know that the component was unmounted and actually not perform the side effects. This is just illustrative saying that, yes, we understand this component knows it's been unmounted and thus, it should not actually perform the side effects.

7. Handling Multiple Actions and UX

Short description:

One final thing to consider in rich client-side applications is the handling of multiple actions on the same screen. It's important to ignore previous requests and only take into account the last sent request. Adding code to improve the user experience can become intrusive, especially when handling errors. Field level validation, blocking input while waiting, and handling network issues are important considerations. Blocking actions prevent users from leaving, while non-blocking actions display toasts when complete. Having a notification center and preventing double submissions are also good practices.

One final thing that can happen with a very rich client-side applications is that when you have a series of actions on the same screen, which render in the same UI but might take a different time to respond from the server. For example, filtering data might have different response times depending on how much data needs to be processed on the server. If the user clicks around many different times, you can see that the responses come out of we don't show the last clicked action result, we actually show the last result that arrived, which might actually be the one that was sent earlier. So special care must be taken to basically ignore all previous requests and only take into account the last sent request.

Let's take a look at some of the code. So on the left side I'm going to keep the very simple, very direct implementation of a handling submission. And on the right side I'm going to add more code to improve the UX and we'll see how much bigger the code becomes and how much more intrusive this UX becomes. So the first thing, we're going to add a state for whether or not we're submitting or not. As you can see, we need to state variable, we need to properly handle it when it starts, when it stops. We need to display the UI that shows that the action is being performed. And then we also want to disable the button. If we want to handle errors it gets even more intrusive because we are touching already touched code and you can see that the UI is also being changed in a very inline way. It's really hard to isolate this in a higher level component.

And as before, there is always more we can add. We can add field level validation where we actually tell the user that some fields were wrong, they can fill them. Or we can tell them that, no, there is something you did wrong, it's the server side error. We can block the input while we wait because we want to prevent them from editing the fields. We want to handle network issues. This is very important in case there is a lot of work done. And if they kind of lost the work, it will be bad for the user. We might save the values. If it's a model, we might want to consider while we're waiting for response, do we allow for leaving and lots of other things. We generally want to think about blocking versus non-blocking actions. Blockings are the actions that prevent you from leaving. Because your actions after this one depend on this one succeeding. There are also non-blocking actions, also called fire and forget. In those cases, we just want to display toasts when the action is complete. Always consider having a notification center or something where the user can look up the previously done actions. In general, we want to prevent double submissions. The simple solution is just to disable the action buttons.

8. Handling Idempotency and Context Loss

Short description:

We want to use idempotency for important scenarios like booking or payments. Handling network issues involves retrying failed requests and informing the user through email or SMS. Idempotency allows safe retry of actions. Context loss occurs when navigating through the application, and we need to handle unmounting components and race conditions to prevent strange UI rendering.

Basically, we want to click it twice. However, that's not always good. If the page is refreshed, the button may be clicked again. The advanced solution is to use idempotency. We really want to use them for really important scenarios, such as booking or payments. They require additional work on the server side, but they will yield the best response for the user and for the UX.

Network issues, as mentioned earlier, we want to handle network issues in two different ways. There might be the outgoing issue, basically, we were unable to send the request to the server, thus failing, so they can retry again. Or actually, we send the request, however, while the server got the request, while the response was traveling back or while we were waiting, in the meantime, the network had some glitches. In that case, the server still processes our request, and the user can actually retry again and thus go into inconsistent state, basically doing the same action twice. We want to see if we can inform the user some other way, like using email notification center or SMS, that the action has been completed. As before, idempotency on the actions allows for safe retry, because you can actually send the same request twice, and the server will not do the same work twice. This requires idempotency, it cannot be done just by analyzing the request, because sometimes it's valid to do the same thing with the same data.

The context loss is the situation where we can navigate through the application, and then when the operation succeeds, something bad might happen. I have some links here you want to take into account when researching this topic. Let's take a look at a simple way of handling whether or not the component is mounted or not. This is a very simple hook that will tell you whether or not the component has been unmounted. We can use it in a very straightforward way by just basically checking if the component has been unmounted. This is a click handler. If we have useEffect, it becomes a little bit trickier because we can do it in three different ways. We can either split the work in the available part, and then make it cancelable, and then handle it in cache, basically separately. As you can see, the logic actually kind of gets split and this is not a really great solution. This can be done and we use the unsubscribed method to stop the asynchronous action. Another way is to use, again, isMounted the hook to know whether or not the component has been unmounted, basically return from the function import subsequent processing. The third way is to use something that's called cooperative cancellation, inspired by C-sharp. This code displays how one would actually create a token in the effect itself and then cancel it in the unsubscribed part. The asynchronous function after every await call must check if the cancellation is requested. Basically after await call, the component might be unmounted and we don't know so we have to check. Another thing we want to handle is the race conditions as shown in the demo. If we reuse the UI after actions, depending on the order of them arriving, this might render a very strange UI.

9. Considerations for Optimistic Updates

Short description:

One final thing to consider is optimistic updates, where we pretend that an action succeeded immediately to make the UI responsive. However, this can lead to inconsistencies if subsequent actions encounter errors. In such cases, consider CRDT or other technologies for collaborative applications.

One final thing before I wrap this up is the optimistic updates. It's very common these days to have a client side caching and basically when initiating an action we actually pretend that the action succeeded immediately so that the UI looks very responsive and then later on we basically synchronize with the server. Please be advised that this might lead into very bad situations if there are subsequent actions that the user takes, because if there are network or any kind of other errors. If you don't handle it correctly in the UI, it can get pretty inconsistent pretty quickly. In that case, consider CRDT or other technologies if you're building for example collaborative applications.

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

React Advanced Conference 2021React Advanced Conference 2021
39 min
Don't Solve Problems, Eliminate Them
Top Content
Humans are natural problem solvers and we're good enough at it that we've survived over the centuries and become the dominant species of the planet. Because we're so good at it, we sometimes become problem seekers too–looking for problems we can solve. Those who most successfully accomplish their goals are the problem eliminators. Let's talk about the distinction between solving and eliminating problems with examples from inside and outside the coding world.
React Day Berlin 2022React Day Berlin 2022
22 min
Jotai Atoms Are Just Functions
Top Content
Jotai is a state management library. We have been developing it primarily for React, but it's conceptually not tied to React. It this talk, we will see how Jotai atoms work and learn about the mental model we should have. Atoms are framework-agnostic abstraction to represent states, and they are basically just functions. Understanding the atom abstraction will help designing and implementing states in your applications with Jotai
React Summit 2023React Summit 2023
24 min
Debugging JS
As developers, we spend much of our time debugging apps - often code we didn't even write. Sadly, few developers have ever been taught how to approach debugging - it's something most of us learn through painful experience.  The good news is you _can_ learn how to debug effectively, and there's several key techniques and tools you can use for debugging JS and React apps.
React Day Berlin 2022React Day Berlin 2022
29 min
Fighting Technical Debt With Continuous Refactoring
Top Content
Let’s face it: technical debt is inevitable and rewriting your code every 6 months is not an option. Refactoring is a complex topic that doesn't have a one-size-fits-all solution. Frontend applications are particularly sensitive because of frequent requirements and user flows changes. New abstractions, updated patterns and cleaning up those old functions - it all sounds great on paper, but it often fails in practice: todos accumulate, tickets end up rotting in the backlog and legacy code crops up in every corner of your codebase. So a process of continuous refactoring is the only weapon you have against tech debt. In the past three years, I’ve been exploring different strategies and processes for refactoring code. In this talk I will describe the key components of a framework for tackling refactoring and I will share some of the learnings accumulated along the way. Hopefully, this will help you in your quest of improving the code quality of your codebases.
React Summit Remote Edition 2020React Summit Remote Edition 2020
32 min
AHA Programming
Top Content
Are you the kind of programmer who prefers to never see the same code in two places, or do you make liberal use of copy/paste? Many developers swear the Don't Repeat Yourself (DRY) philosophy while others prefer to Write Everything Twice (WET). But which of these produces more maintainable codebases? I've seen both of these approaches lay waste to codebases and I have a new ideology I would like to propose to you: Avoid Hasty Abstractions (AHA). In this keynote, we'll talk about abstraction and how you can improve a codebase applying and creating abstractions more thoughtfully as well as how to get yourself out of a mess of over or under-abstraction.
React Summit US 2023React Summit US 2023
21 min
The Epic Stack
Modern web development is fantastic. There are so many great tools available! Modern web development is exhausting. There are so many great tools available! Each of these sentiments is true. What's great is that most of the time, it's hard to make a choice that is wrong. Seriously. The trade-offs of most of the frameworks and tools you could use to build your application fit within the constraints of the vast majority of apps. Despite this, engineers consistently struggle with analysis paralysis.Let's talk about this, and a solution I am working on for it.

Workshops on related topic

React Advanced Conference 2021React Advanced Conference 2021
174 min
React, TypeScript, and TDD
Top Content
Featured WorkshopFree
ReactJS is wildly popular and thus wildly supported. TypeScript is increasingly popular, and thus increasingly supported.

The two together? Not as much. Given that they both change quickly, it's hard to find accurate learning materials.

React+TypeScript, with JetBrains IDEs? That three-part combination is the topic of this series. We'll show a little about a lot. Meaning, the key steps to getting productive, in the IDE, for React projects using TypeScript. Along the way we'll show test-driven development and emphasize tips-and-tricks in the IDE.
React Advanced Conference 2021React Advanced Conference 2021
145 min
Web3 Workshop - Building Your First Dapp
Top Content
Featured WorkshopFree
In this workshop, you'll learn how to build your first full stack dapp on the Ethereum blockchain, reading and writing data to the network, and connecting a front end application to the contract you've deployed. By the end of the workshop, you'll understand how to set up a full stack development environment, run a local node, and interact with any smart contract using React, HardHat, and Ethers.js.
React Summit 2022React Summit 2022
136 min
Remix Fundamentals
Top Content
Featured WorkshopFree
Building modern web applications is riddled with complexity And that's only if you bother to deal with the problems
Tired of wiring up onSubmit to backend APIs and making sure your client-side cache stays up-to-date? Wouldn't it be cool to be able to use the global nature of CSS to your benefit, rather than find tools or conventions to avoid or work around it? And how would you like nested layouts with intelligent and performance optimized data management that just works™?
Remix solves some of these problems, and completely eliminates the rest. You don't even have to think about server cache management or global CSS namespace clashes. It's not that Remix has APIs to avoid these problems, they simply don't exist when you're using Remix. Oh, and you don't need that huge complex graphql client when you're using Remix. They've got you covered. Ready to build faster apps faster?
At the end of this workshop, you'll know how to:- Create Remix Routes- Style Remix applications- Load data in Remix loaders- Mutate data with forms and actions
Vue.js London Live 2021Vue.js London Live 2021
169 min
Vue3: Modern Frontend App Development
Top Content
Featured WorkshopFree
The Vue3 has been released in mid-2020. Besides many improvements and optimizations, the main feature of Vue3 brings is the Composition API – a new way to write and reuse reactive code. Let's learn more about how to use Composition API efficiently.

Besides core Vue3 features we'll explain examples of how to use popular libraries with Vue3.

Table of contents:
- Introduction to Vue3
- Composition API
- Core libraries
- Vue3 ecosystem

IDE of choice (Inellij or VSC) installed
Nodejs + NPM
JSNation 2023JSNation 2023
174 min
Developing Dynamic Blogs with SvelteKit & Storyblok: A Hands-on Workshop
Featured WorkshopFree
This SvelteKit workshop explores the integration of 3rd party services, such as Storyblok, in a SvelteKit project. Participants will learn how to create a SvelteKit project, leverage Svelte components, and connect to external APIs. The workshop covers important concepts including SSR, CSR, static site generation, and deploying the application using adapters. By the end of the workshop, attendees will have a solid understanding of building SvelteKit applications with API integrations and be prepared for deployment.
React Summit 2023React Summit 2023
106 min
Back to the Roots With Remix
Featured Workshop
The modern web would be different without rich client-side applications supported by powerful frameworks: React, Angular, Vue, Lit, and many others. These frameworks rely on client-side JavaScript, which is their core. However, there are other approaches to rendering. One of them (quite old, by the way) is server-side rendering entirely without JavaScript. Let's find out if this is a good idea and how Remix can help us with it?
Prerequisites- Good understanding of JavaScript or TypeScript- It would help to have experience with React, Redux, Node.js and writing FrontEnd and BackEnd applications- Preinstall Node.js, npm- We prefer to use VSCode, but also cloud IDEs such as codesandbox (other IDEs are also ok)