Back to Riverpod

How to debounce/cancel network requests

website/docs/how_to/cancel.mdx

2.0.0-dev.95.9 KB
Original Source

import { Link } from "/src/components/Link"; import { AutoSnippet, When } from "/src/components/CodeSnippet"; import homeScreen from "!raw-loader!./cancel/home_screen.dart"; import extension from "!raw-loader!./cancel/extension.dart"; import detailScreen from "./cancel/detail_screen"; import detailScreenCancel from "./cancel/detail_screen_cancel"; import detailScreenDebounce from "./cancel/detail_screen_debounce"; import providerWithExtension from "./cancel/provider_with_extension";

As applications grow in complexity, it's common to have multiple network requests in flight at the same time. For example, a user might be typing in a search box and triggering a new request for each keystroke. If the user types quickly, the application might have many requests in flight at the same time.

Alternatively, a user might trigger a request, then navigate to a different page before the request completes. In this case, the application might have a request in flight that is no longer needed.

To optimize performance in those situations, there are a few techniques you can use:

  • "Debouncing" requests. This means that you wait until the user has stopped typing for a certain amount of time before sending the request. This ensures that you only send one request for a given input, even if the user types quickly.
  • "Cancelling" requests. This means that you cancel a request if the user navigates away from the page before the request completes. This ensures that you don't waste time processing a response that the user will never see.

In Riverpod, both of these techniques can be implemented in a similar way. The key is to use ref.onDispose combined with "automatic disposal" or ref.watch to achieve the desired behavior.

To showcase this, we will make a simple application with two pages:

  • A home screen, with a button which opens a new page
  • A detail page, which displays a random activity from the Bored API, with the ability to refresh the activity.
    See <Link documentID="how_to/pull_to_refresh" /> for information on how to implement pull-to-refresh.

We will then implement the following behaviors:

  • If the user opens the detail page and then navigates back immediately, we will cancel the request for the activity.
  • If the user refreshes the activity multiple times in a row, we will debounce the requests so that we only send one request after the user stops refreshing.

The application

First, let's create the application, without any debouncing or cancelling.
We won't use anything fancy here, and stick to a plain FloatingActionButton with a Navigator.push to open the detail page.

First, let's start with defining our home screen. As usual, let's not forget to specify a ProviderScope at the root of our application.

<AutoSnippet title="lib/src/main.dart" raw={homeScreen} />

Then, let's define our detail page. To fetch the activity and implement pull-to-refresh, refer to the <Link documentID="how_to/pull_to_refresh" /> case study.

<AutoSnippet title="lib/src/detail_screen.dart" {...detailScreen} />

Cancelling requests

Now that we have a working application, let's implement the cancellation logic.

To do so, we will use ref.onDispose to cancel the request when the user navigates away from the page. For this to work, it is important that the automatic disposal of providers is enabled.

The exact code needed to cancel the request will depend on the HTTP client. In this example, we will use package:http, but the same principle applies to other clients.

The key here is that ref.onDispose will be called when the user navigates away. That is because our provider is no-longer used, and therefore disposed thanks to automatic disposal.
We can therefore use this callback to cancel the request. When using package:http, this can be done by closing our HTTP client.

<AutoSnippet {...detailScreenCancel} />

Debouncing requests

Now that we have implemented cancellation, let's implement debouncing.
At the moment, if the user refreshes the activity multiple times in a row, we will send a request for each refresh.

Technically speaking, now that we have implemented cancellation, this is not a problem. If the user refreshes the activity multiple times in a row, the previous request will be cancelled, when a new request is made.

However, this is not ideal. We are still sending multiple requests, and wasting bandwidth and server resources.
What we could instead do is delay our requests until the user stops refreshing the activity for a fixed amount of time.

The logic here is very similar to the cancellation logic. We will again use ref.onDispose. However, the idea here is that instead of closing an HTTP client, we will rely on onDispose to abort the request before it starts.
We will then arbitrarily wait for 500ms before sending the request. Then, if the user refreshes the activity again before the 500ms have elapsed, onDispose will be invoked, aborting the request.

:::info To abort requests, a common practice is to voluntarily throw.
It is safe to throw inside providers after the provider has been disposed. The exception will naturally be caught by Riverpod and be ignored. :::

<AutoSnippet {...detailScreenDebounce} />

Going further: Doing both at once

We now know how to debounce and cancel requests.
But currently, if we want to do another request, we need to copy-paste the same logic in multiple places. This is not ideal.

However, we can go further and implement a reusable utility to do both at once.

The idea here is to implement an extension method on Ref that will handle both cancellation and debouncing in a single method.

<AutoSnippet raw={extension} />

We can then use this extension method in our providers as followed:

<AutoSnippet {...providerWithExtension} />