Skip to content
Back to skills

Riverpod Infinite Rebuild Loop

ASecurity

Debug and fix infinite widget rebuild loops in Flutter apps using Riverpod state management. Use when: (1) Logs show "RAPID REBUILD" warnings or 50+ rebuilds in seconds, (2) UI becomes unresponsive or shows same content repeatedly, (3) Bug only affects slower devices or poor network conditions, (4) Provider watching creates circular dependencies with router/URL state. Covers ref.watch() overuse, watch+listener redundancy, and transitive dependency chains.

  • 265 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added May 27, 2026
developmentgoreactapiperformance

Works with

  • claude code
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add divinevideo/divine-mobile --skill riverpod-infinite-rebuild-loop --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Riverpod Infinite Rebuild Loop?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Riverpod Infinite Rebuild Loop
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/divinevideo-riverpod-infinite-rebuild-loop/badge)](https://www.skillsdirectory.com/skills/divinevideo-riverpod-infinite-rebuild-loop)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: riverpod-infinite-rebuild-loop
description: |
  Debug and fix infinite widget rebuild loops in Flutter apps using Riverpod state management.
  Use when: (1) Logs show "RAPID REBUILD" warnings or 50+ rebuilds in seconds, (2) UI becomes
  unresponsive or shows same content repeatedly, (3) Bug only affects slower devices or poor
  network conditions, (4) Provider watching creates circular dependencies with router/URL state.
  Covers ref.watch() overuse, watch+listener redundancy, and transitive dependency chains.
author: Claude Code
version: 1.0.0
date: 2026-02-02
---

# Riverpod Infinite Rebuild Loop

## Problem

Flutter widgets using Riverpod enter an infinite rebuild loop, causing:
- 50+ widget rebuilds in seconds
- UI becomes unresponsive
- Same content rendered repeatedly
- Bug manifests primarily on slower devices or poor network connections

## Context / Trigger Conditions

**Symptoms in logs:**
```
⚠️ RAPID REBUILD #54! Only 15ms since last build
⚠️ RAPID REBUILD DETECTED! Only 6ms since last build
```

**User-reported symptoms:**
- "Endless scroll loop" - same video/content keeps appearing
- "App freezes" or becomes unresponsive
- "Works on my phone but not on older devices"

**Trigger scenarios:**
1. Multiple async providers completing at staggered times
2. Provider that watches route/URL state AND updates URL in build
3. Using `ref.watch()` AND adding manual listeners to same provider
4. Transitive watches: Widget watches A, A watches B, Widget also watches B

## Root Cause Analysis

### Pattern 1: URL Update Feedback Loop

```dart
// BAD: Creates infinite loop
Widget build(BuildContext context) {
  final pageContext = ref.watch(pageContextProvider);  // Watches URL
  final videos = ref.watch(videosProvider);

  // Detect video moved position and "silently" update URL
  if (currentVideoIndex != urlIndex) {
    context.go('/home/$currentVideoIndex');  // URL change triggers rebuild!
  }
}
```

**Loop:** Videos reorder → URL updated → pageContextProvider emits → rebuild → videos may reorder again → repeat

### Pattern 2: Watch + Listener Redundancy

```dart
// BAD: Double-subscribing causes double rebuilds
final cache = ref.watch(cacheProvider);  // Watch triggers rebuild
cache.addListener(onCacheChanged);        // Listener ALSO triggers action
```

### Pattern 3: Transitive Watch Dependencies

```dart
// BAD: Double-watching same source
Widget build() {
  ref.watch(pageContextProvider);           // Watch #1
  ref.watch(derivedProvider);               // derivedProvider ALSO watches pageContextProvider!
}
```

### Pattern 4: Staggered Async Provider Loading

```dart
// PROBLEMATIC on slow devices: Each watch triggers rebuild when provider completes
final a = ref.watch(asyncProviderA);  // Completes at T=100ms → rebuild
final b = ref.watch(asyncProviderB);  // Completes at T=200ms → rebuild
final c = ref.watch(asyncProviderC);  // Completes at T=350ms → rebuild
final d = ref.watch(asyncProviderD);  // Completes at T=500ms → rebuild
// On fast devices: all complete ~simultaneously, 1-2 rebuilds
// On slow devices: staggered completion, 4+ rebuilds
```

## Solution

### Step 1: Audit `ref.watch()` Usage

For each `ref.watch()` in build methods, ask:
- Does this provider change frequently?
- Do I need to REBUILD when it changes, or just REACT?
- Am I also manually listening to this provider?

**Riverpod Methods:**
| Method | Behavior |
|--------|----------|
| `ref.watch()` | Subscribe + **REBUILD** on change |
| `ref.read()` | Read once, **NO rebuild** |
| `ref.listen()` | Subscribe + callback, **NO rebuild** |

### Step 2: Convert Unnecessary Watches

```dart
// BEFORE: Rebuilds on every change
final videoService = ref.watch(videoServiceProvider);

// AFTER: Read once, use listener for reactions
final videoService = ref.read(videoServiceProvider);
ref.listen(videoServiceProvider, (prev, next) {
  // React to changes without rebuilding
  if (next.hasNewVideos) refreshUI();
});
```

### Step 3: Remove Watch + Listener Redundancy

```dart
// BEFORE: Double-subscription
final cache = ref.watch(cacheProvider);
cache.addListener(onCacheChanged);

// AFTER: Choose one approach
// Option A: Just watch (if rebuild is needed)
final cache = ref.watch(cacheProvider);

// Option B: Read + listen (if rebuild not needed)
final cache = ref.read(cacheProvider);
ref.listen(cacheProvider, (_, __) => onCacheChanged());
```

### Step 4: Break URL Update Loops

```dart
// BEFORE: URL update in build causes loop
if (currentVideoIndex != urlIndex) {
  context.go('/home/$currentVideoIndex');
}

// AFTER: Don't update URL on content reorder
// Just track position with PageController, not URL
// OR use content ID in URL instead of index: /home/video/abc123
```

### Step 5: Batch Initial Load

```dart
// BEFORE: Watch each async provider (N rebuilds on slow devices)
final a = ref.watch(asyncA);
final b = ref.watch(asyncB);

// AFTER: Create combined provider that waits for all
@riverpod
Future<CombinedState> combinedState(Ref ref) async {
  final a = await ref.watch(asyncA.future);
  final b = await ref.watch(asyncB.future);
  return CombinedState(a, b);
}
// Widget watches only the combined provider (1 rebuild)
```

## Verification

After fixes:
1. Run app on slow device or use network throttling
2. Check logs for rebuild warnings - should see ≤5 rebuilds on startup
3. Scroll/navigate and verify UI remains responsive
4. No "RAPID REBUILD" warnings in logs

## Example: Full Fix

**Before (problematic):**
```dart
class _HomeScreenState extends ConsumerState<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    final pageContext = ref.watch(pageContextProvider);
    final videos = ref.watch(videosProvider);  // Also watches pageContextProvider internally!

    // URL update loop
    if (videos.currentIndex != pageContext.index) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        context.go('/home/${videos.currentIndex}');
      });
    }

    return PageView(...);
  }
}
```

**After (fixed):**
```dart
class _HomeScreenState extends ConsumerState<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    // Only watch videos, read page context
    final pageContext = ref.read(pageContextProvider).requireValue;
    final videos = ref.watch(videosProvider);

    // Don't update URL on reorder - track with PageController only
    // URL only changes on explicit user navigation

    return PageView(...);
  }
}
```

## Notes

- This bug is **timing-dependent** - may not reproduce on fast devices
- Test on physical devices with network throttling to catch issues
- Add rebuild detection logging during development:
  ```dart
  static int _buildCount = 0;
  static DateTime? _lastBuild;

  @override
  Widget build(BuildContext context) {
    final now = DateTime.now();
    if (_lastBuild != null && now.difference(_lastBuild!).inMilliseconds < 100) {
      Log.warning('RAPID REBUILD #${++_buildCount}!');
    }
    _lastBuild = now;
    // ...
  }
  ```

## References

- [Riverpod: Reading Providers](https://riverpod.dev/docs/concepts/reading)
- [Riverpod: ref.watch vs ref.read vs ref.listen](https://riverpod.dev/docs/essentials/combining_providers)
- [Flutter DevTools: Identify Rebuilds](https://docs.flutter.dev/tools/devtools/performance)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.