skip to content

In Riverpod 3, how does provider.select narrow a widget's rebuilds, and what does it not save you from?

level: middleimportance: should knowfreq 46%

answer

  1. selector runs on every notification
  2. compared with ==
  3. new List each time defeats it
  4. upstream still recomputes
  5. selectAsync for futures

basics

~20 s

provider.select(fn) makes ref.watch or ref.listen react only when fn's result changes by ==. The upstream provider still recomputes and the selector still runs on every change; returning a freshly built list or object each time defeats it.

solid answer

~40 s

`ref.watch(portfolioProvider.select((p) => p.total))` subscribes to a derived value: on each notification from `portfolioProvider` Riverpod runs the selector and notifies the widget only if the new result is not `==` to the previous one. It is the documented way to cut rebuilds instead of `ref.read`. It does not stop the upstream provider from recomputing, and the selector runs on every notification, so it should be cheap. The comparison is `==`, so a selector that builds a new `List` with `toList()` rebuilds every time unless the result type has value equality. Since Riverpod 3.0 all providers themselves also filter updates with `==`. For async providers, `selectAsync` gives a `Future` of the selected value, typically awaited inside another provider.

code

dart · 22 lines
dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class TotalLabel extends ConsumerWidget {
  const TotalLabel({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final total = ref.watch(portfolioProvider.select((p) => p.total));
    final currency = ref.watch(portfolioProvider.select((p) => p.currency));

    ref.listen(portfolioProvider.select((p) => p.currency), (previous, next) {
      if (previous != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Converted from $previous to $next')),
        );
      }
    });

    return Text('${total.toStringAsFixed(2)} $currency');
  }
}

go deeper

for a junior

Remember that select lets a widget watch one field of a provider's state instead of the whole object.

for a middle

Walk through the mechanics: selector runs per notification, result compared with ==, rebuild only on difference. Explain why a new List per call defeats it.

for a senior

Decide between select, a derived provider and a plain watch from profiling evidence, and connect Riverpod 3's == filtering to value-equality state classes.

for a principal

Guide the team away from premature select sprinkling; set equality conventions for state classes so filtering works predictably across the codebase.

## The problem select solves In a stock-portfolio app, a `portfolioProvider` may expose one state object with a `total`, a `currency` and a list of `positions`. A header that only shows the total would, with a plain `ref.watch(portfolioProvider)`, rebuild whenever any position ticks. **`select`** narrows the subscription to the part the widget uses. ```dart final total = ref.watch( portfolioProvider.select((p) => p.total), ); ``` `select` is an extension on `ProviderListenable`, so it works with `ref.watch`, `ref.listen`, `ref.read` and in both widgets and providers. It returns another `ProviderListenable` of the selected type. ## What happens on each change 1. `portfolioProvider` produces a new state and, if the new state is not `==` to the old one, notifies its listeners. 2. For the selected listener, Riverpod runs the selector on the new state. 3. It compares the result with the last selected result using `==`. 4. Only if they differ is the widget marked to rebuild (or the `listen` callback called). So the header rebuilds when the total changes, and not when a position's price moves in a way that leaves the total identical. ## What select does not do | Assumption | Reality | |---|---| | The upstream provider stops recomputing | It recomputes exactly as before; only the downstream notification is filtered | | The selector runs only when the field changes | It runs on every upstream notification, so keep it cheap | | Any derived value is filtered | Only values with meaningful `==`; a new `List` per call compares by identity | | It replaces splitting providers | For expensive derivations, a separate `Provider` that watches the source caches the result once for all readers | The `==` trap is the one interviewers probe. This selector rebuilds on every change: ```dart ref.watch(portfolioProvider.select( (p) => p.positions.where((x) => x.quantity > 0).toList(), )); ``` `toList()` returns a new `List` every time, and Dart's `List` uses identity equality, so the comparison always fails. Fixes: select a primitive (a count, a total), give the result a value-equality type, or move the derivation into its own `Provider` that watches the source. ## Equality in Riverpod 3 Riverpod 3.0 made **all providers** filter their own notifications with `==`; before, some used `identical`. For state classes with value equality (hand-written `==`, freezed or equatable), an update that produces an equal object no longer notifies at all. Notifiers can override `updateShouldNotify` if a different rule is needed. `select` then applies the same `==` test to the selected slice. ## Async providers: selectAsync For a `FutureProvider` or `AsyncNotifierProvider`, plain `select` sees `AsyncValue` objects. `selectAsync` on the provider returns a `ProviderListenable<Future<Out>>`, which fits inside another provider: ```dart final totalInBaseProvider = FutureProvider((ref) async { final total = await ref.watch( quotesProvider.selectAsync((q) => q.total), ); return total; }); ``` The dependent provider re-runs only when the selected value changes. ## When to reach for it - A widget reads one field of a large or frequently changing state. - A `ref.listen` should fire only on a specific transition, such as the currency field changing. - Not as a reflex: the Riverpod docs warn against over-optimising, and a plain `watch` is fine for most widgets.

  • Is select better than creating a separate derived Provider?
    They solve different costs. `select` filters rebuilds of one listener but runs its selector per listener on every change. A derived `Provider` that watches the source computes once, caches the result for every reader, and notifies only when its result changes by `==`. For a cheap field access, `select`; for an expensive filter or sort shared by several widgets, a derived provider.
  • What changed about update filtering in Riverpod 3.0?
    All providers now use `==` to decide whether to notify; before, some used `identical`. A stream or notifier that emits an equal value no longer notifies listeners. A `Notifier` can override `updateShouldNotify` to change this, for example to `identical` for a large class with an expensive `==`.

saying these in an interview costs you the question

  • select stops the upstream provider from recomputing
  • The selector only runs when the chosen field changes
  • Returning a new toList() from select is fine because contents are compared
  • ref.read in build is the better way to avoid rebuilds
  • select only works inside widgets, not inside providers