skip to content

In Riverpod 3, how does overriding a provider in a nested ProviderScope scope it to a subtree, and why must it declare dependencies?

level: seniorimportance: should knowfreq 28%

answer

  1. a scope below the root
  2. dependencies opts a provider in
  3. dependants must list scoped providers
  4. per-item overrideWithValue in lists
  5. tester.container(of:) finds the nested one

basics

~20 s

A nested ProviderScope creates a child container whose overrides apply only to its subtree. A provider opts into scoping by declaring dependencies, and every provider watching it must list it, or that dependant stays in the root container and never sees the override.

solid answer

~40 s

Scoping means changing a provider for part of the widget tree: a `ProviderScope` below the root creates a child container, and overrides placed there apply only to widgets beneath it. A typical use is a list where each `QuestionCard` is wrapped in `ProviderScope(overrides: [currentQuestionIdProvider.overrideWithValue(id)])`, so the card reads its id without constructor parameters. A provider opts in with `dependencies` — `dependencies: const []` for the first scoped one — and any provider that watches it must list it, e.g. `dependencies: [currentQuestionIdProvider]`. Riverpod then re-creates that dependant in the nested container. If a dependant omits it, it is mounted in the root container and reads the root value, so the override seems ignored. In widget tests, `tester.container(of: find.byType(QuestionCard).first)` returns the nested container. Riverpod's docs call scoping highly complex and likely to be reworked.

code

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

// Scoped placeholder: every QuestionCard overrides it.
final currentQuestionIdProvider = Provider<String>(
  (ref) => throw UnimplementedError('Override per QuestionCard'),
  dependencies: const [],
);

// Watches a scoped provider, so it must list it.
final questionTextProvider = Provider<String>(
  (ref) => 'Question ${ref.watch(currentQuestionIdProvider)}',
  dependencies: [currentQuestionIdProvider],
);

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Text(ref.watch(questionTextProvider));
  }
}

class QuizList extends StatelessWidget {
  const QuizList({super.key, required this.ids});
  final List<String> ids;

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        for (final id in ids)
          ProviderScope(
            overrides: [currentQuestionIdProvider.overrideWithValue(id)],
            child: const QuestionCard(),
          ),
      ],
    );
  }
}

go deeper

for a junior

Recall that a ProviderScope below the root can override a provider for its subtree, for example one id per list item.

for a middle

Explain dependencies as the opt-in, why dependants must list scoped providers, and how per-item overrideWithValue replaces constructor parameters.

for a senior

Diagnose ignored overrides caused by missing dependencies, test scoped subtrees with the right container, and judge when a family is the simpler design.

for a principal

Weigh scoping's convenience against its documented complexity and likely rework, and set a policy on where the team allows it.

## What scoping is **Scoping** is changing a provider's behaviour for only part of the app. Riverpod achieves it with ordinary overrides placed in a `ProviderScope` that is **not** the root: that scope creates a child `ProviderContainer` whose parent is the nearest ancestor container, and its overrides apply only to widgets below it. Riverpod's documentation lists three uses: - page- or widget-specific customisation, such as a different theme for one page; - performance, such as rebuilding only the list item that changed; - avoiding parameter passing, as an alternative to families. The same docs warn that the feature is highly complex and will likely be reworked, and advise care. ## Opting in with `dependencies` By default Riverpod does not treat a provider as scopable. The `dependencies` parameter is the opt-in. The source's documentation describes it as strictly equivalent to saying *this provider may be scoped*: 1. The first scoped provider declares `dependencies: const []` — often a placeholder whose create function throws, because every real use overrides it. 2. Every provider that watches a scoped provider must declare `dependencies` and **include** that scoped provider. 3. Providers that only watch unscoped providers need no `dependencies` at all; listing an unscoped provider is a no-op. ## Why the list matters at runtime When a child container is created, Riverpod uses the transitive `dependencies` to decide **where each provider is mounted**. A provider whose dependencies include something overridden in the child is re-created inside the child container, so it sees the scoped value. A provider with no `dependencies` is mounted in the root container, where the scoped provider has its root behaviour. | Setup | Where the dependant lives | What it reads | |---|---|---| | dependant lists the scoped provider | the nested container | the scoped override | | dependant omits `dependencies` | the root container | the root value, often the throwing placeholder | | widget watches the scoped provider directly | its nearest container | the scoped override | The middle row is the classic *my override is ignored* bug. riverpod_lint catches it for generated providers. ## A quiz-list example In a quiz app, a `QuizList` renders a `QuestionCard` per question. Instead of passing the id through constructors or making every provider a family, each card is wrapped in `ProviderScope(overrides: [currentQuestionIdProvider.overrideWithValue(id)], child: const QuestionCard())`. A `questionTextProvider` that watches `currentQuestionIdProvider` lists it in `dependencies`, so each card gets its own instance. `overrideWithValue` suits this well: when the list rebuilds with a different id for the same scope, the value override is updated and listeners are notified. Note that `overrideWith` and `overrideWithValue` both **disable auto-scoping for the overridden provider itself** — its own `dependencies` no longer apply once it is replaced. ## Scoped providers in tests Scoping and testing meet in two places: - **Testing a scoped subtree.** Pump the subtree inside a `ProviderScope` that overrides the scoped provider, exactly as the parent widget would, so the placeholder never runs. - **Reading a nested container.** `tester.container()` looks for a single scope and fails when several match, as in a list of cards. Pass a finder instead: `tester.container(of: find.byType(QuestionCard).first)` returns the container nearest that widget, the nested one. In pure Dart tests, `ProviderContainer.test(parent: root, overrides: [...])` builds the same parent-child relationship without widgets. ## Scoping as a test tool Nested scopes also help tests that are not about scoping. A widget test can pump the whole app under a root `ProviderScope` with the repository fake, then wrap one screen in a nested `ProviderScope` that overrides only a scoped provider, such as the current question id, to start that screen at a chosen question. The nested container inherits every override from its parent, so the fake repository still applies below it; only the providers overridden in the nested scope, and the dependants that list them, differ. ## Pitfalls - A dependant that forgets `dependencies` silently reads the root value. - A scoped placeholder that throws surfaces as an error wherever something reads it outside an overriding scope. - Changing the number of overrides on a nested scope between rebuilds asserts; only existing overrides can be updated. - Overusing scoping where a family would do makes data flow hard to follow; prefer families unless avoiding parameter plumbing is the point.

  • When would you use a family instead of scoping for per-item state?
    Most of the time. A family such as `questionTextProvider(id)` makes the argument explicit at every call site and needs no dependencies bookkeeping. Scoping pays off when many deeply nested widgets and providers need the same contextual value and threading it through as an argument would clutter every layer, which Riverpod's docs list as avoiding parameter passing.
  • Why does a scoped placeholder usually throw in its create function?
    Because it has no meaningful root value: every legitimate use overrides it in a nested scope. Throwing, for example `UnimplementedError`, makes a missing override fail loudly at the first read instead of silently showing a default. The same pattern shows up in tests, which must override it in the scope they pump.

saying these in an interview costs you the question

  • Any provider can be scoped by overriding it in a nested ProviderScope, with no opt-in.
  • A provider that watches a scoped one picks up the override without listing it.
  • tester.container() always returns the innermost scope around the widget under test.
  • Nested scopes copy all state from the parent, so overrides there reset every provider.
  • Adding a new override to a nested scope on rebuild is fine.