Repository: rrousselGit/riverpod

Issue:
Error: setState() or markNeedsBuild() called during build.
**Describe the bug**
When creating a special dependency tree, updating a provider which is depended upon can cause `setState` to be called during build.

**To Reproduce**

The setup needed to reproduce the crash is the following:
*  **Use Flutter web**
* Provide a `parent` to `ProviderScope`
* Create 2 providers: `Provider1` which can be updated. `Provider2` which `watches` `Provider1`
* `watch` `Provider2` into a `ParentWidget`
* In the subtree of `ParentWidget`, place a `Widget` which watches `Provider1` **and** `Provider2`
* In the subtree of `Widget`, place a `ChildWidget` which updates `Provider1` in a `TextField.onChanged`

Note: Things can be added (other providers, dependencies, widgets, ...) but if anything of what is listed above is removed, the crash will not appear.

**<details><summary>Example</summary>**

Zapp link: https://zapp.run/edit/flutter-zhq06qhhr06?entry=lib/main.dart&file=lib/main.dart

Code:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final idFilterProvider = StateProvider<String>((ref) => '');

final filteredIdsProvider = Provider<List<String>>(
  (ref) {
    final ids = List.generate(100, (index) => '$index');

    return ids
        .where((id) => id.contains(ref.watch(idFilterProvider)))
        .toList();
  },
  dependencies: [idFilterProvider],
);

main() {
  runApp(
    ProviderScope(
      parent: ProviderContainer(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final filteredIds = ref.watch(filteredIdsProvider);

    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Material(
        child: Column(
          children: [
            const IdFilterWidget(),
            Expanded(
              child: ListView.builder(
                itemCount: filteredIds.length,
                itemBuilder: (context, index) {
                  return ListTile(title: Text(filteredIds[index]));
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class IdFilterWidget extends ConsumerWidget {
  const IdFilterWidget({
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final filteredIds = ref.watch(filteredIdsProvider);
    final idFilter = ref.watch(idFilterProvider);

    return Row(
      children: [
        Expanded(
          child: TextField(
            onChanged: (value) {
              ref.read(idFilterProvider.notifier).state = value;
            },
          ),
        ),

        // The bug can also be triggered by using a button if:
        //   * some operation is performed before updating the state
        //   * debug mode is used
        // Expanded(
        //   child: ElevatedButton(
        //     onPressed: () {
        //       for (var i = 0; i < 1000000000; i++) {}
        //
        //       ref.read(idFilterProvider.notifier).state = 'value';
        //     },
        //     child: Text('Filter: $idFilter'),
        //   ),
        // ),

        Text('Filter value: $idFilter'),
        const SizedBox(width: 16),
        Text('Match count: ${filteredIds.length}'),
      ],
    );
  }
}
```
</details>

**Error**
```
Error: setState() or markNeedsBuild() called during build.
This MyApp widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
  MyApp
The widget which was currently being built when the offending call was made was:
  IdFilterWidget
```

**<details><summary>Stacktrace</summary>**
```
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 266:49      throw_
packages/flutter/src/widgets/framework.dart 4549:11                               <fn>
packages/flutter/src/widgets/framework.dart 4563:14                               markNeedsBuild
packages/flutter_riverpod/src/consumer.dart 508:20                                <fn>
dart-sdk/lib/async/zone.dart 1598:10                                              runBinaryGuarded
packages/riverpod/src/framework/element.dart 510:24                               <fn>
packages/riverpod/src/result.dart 60:16                                           map
packages/riverpod/src/framework/element.dart 507:13                               [_notifyListeners]
packages/riverpod/src/framework/element.dart 364:7                                [_performBuild]
packages/riverpod/src/framework/element.dart 315:7                                flush
packages/riverpod/src/framework/element.dart 766:5                                readSelf
packages/riverpod/src/framework/provider_base.dart 175:29                         read
packages/flutter_riverpod/src/consumer.dart 508:20                                watch
packages/flutter_app_to_delete/main.dart 63:33                                    build
packages/flutter_riverpod/src/consumer.dart 427:19                                build
packages/flutter/src/widgets/framework.dart 4992:27                               build
packages/flutter_riverpod/src/consumer.dart 488:20                                build
packages/flutter/src/widgets/framework.dart 4878:15                               performRebuild
packages/flutter/src/widgets/framework.dart 5050:11                               performRebuild
packages/flutter/src/widgets/framework.dart 4604:5                                rebuild
packages/flutter/src/widgets/framework.dart 2667:18                               buildScope
packages/flutter/src/widgets/binding.dart 882:9                                   drawFrame
packages/flutter/src/rendering/binding.dart 378:5                                 [_handlePersistentFrameCallback]
packages/flutter/src/scheduler/binding.dart 1175:15                               [_invokeFrameCallback]
packages/flutter/src/scheduler/binding.dart 1104:9                                handleDrawFrame
packages/flutter/src/scheduler/binding.dart 1015:5                                [_handleDrawFrame]
lib/_engine/engine/platform_dispatcher.dart 1168:13                               invoke
lib/_engine/engine/platform_dispatcher.dart 219:5                                 invokeOnDrawFrame
lib/_engine/engine/initialization.dart 195:45                                     <fn>
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 334:14  _checkAndCall
dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 339:39  dcall
```
</details>


Discussion: Thanks for the nice reproducible example. I'm able to reproduce it

I'm not yet clear on what happens. I'll investigate 
Hello, I encountered a similar problem. I was using version 2.0.0-dev.9 before and had no problem.

However, today I upgraded to version 2.0.2 and started getting errors.

I have a stateprovider like this.

`final gender = StateProvider.autoDispose<String>((ref) => "");
`
and I was using it like this in InitState, it was working before, but since I upgraded I get an error when I use it like this.

```
 @override
  void initState() {
    super.initState();
ref.read(gender.notifier).state = "Female";
  }
```

If I use this code

`WidgetsBinding.instance.addPostFrameCallback((_) async {}`

 it works but I didn't need it before.


@seceba your case is expected. You're not allowed to modify providers within `initState`.
Ok, so apparently modifying `idFilterProvider` doesn't cause `MyApp` to rebuild even though it's a transitive dependency.  
As such IdFilterWidget rebuilds but MyApp doesn't. And when IdFilterWidget rebuilds, this recomputes filteredIdsProvider, which notifies MyApp that it should rebuild.


I don't know yet why MyApp isn't initially notified though. After-all, the same code works on mobile.
Oh, I get it  now. That `parent: ProviderContainer()` definitely is the issue.

This unsyncs the provider tree from the widget tree. Somehow on mobile the event loop is fast enough that this isn't a problem, but on web it is.

I'll see what I can do.

Fix patch:
diff --git a/packages/flutter_riverpod/lib/src/framework.dart b/packages/flutter_riverpod/lib/src/framework.dart
index bf8e7488a9..4f0152794e 100644
--- a/packages/flutter_riverpod/lib/src/framework.dart
+++ b/packages/flutter_riverpod/lib/src/framework.dart
@@ -1,9 +1,12 @@
+// ignore_for_file: invalid_use_of_internal_member
+
 import 'package:flutter/foundation.dart' hide describeIdentity;
 import 'package:flutter/material.dart';
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/widgets.dart';
 import 'package:meta/meta.dart';
-import 'package:riverpod/riverpod.dart';
+
+import 'internals.dart';
 
 /// {@template riverpod.providerscope}
 /// A widget that stores the state of providers.
@@ -299,14 +302,10 @@ class _UncontrolledProviderScopeElement extends InheritedElement {
   @override
   void mount(Element? parent, Object? newSlot) {
     if (kDebugMode) {
-      _containerOf(widget).debugCanModifyProviders = _debugCanModifyProviders;
+      debugCanModifyProviders ??= _debugCanModifyProviders;
     }
-    assert(
-      _containerOf(widget).vsyncOverride == null,
-      'The ProviderContainer was already associated with a different widget',
-    );
-    _containerOf(widget).vsyncOverride = _flutterVsync;
 
+    vsyncOverride ??= _flutterVsync;
     super.mount(parent, newSlot);
   }
 
@@ -322,24 +321,6 @@ class _UncontrolledProviderScopeElement extends InheritedElement {
     );
   }
 
-  @override
-  void update(ProxyWidget newWidget) {
-    if (kDebugMode) {
-      _containerOf(widget).debugCanModifyProviders = null;
-      _containerOf(newWidget).debugCanModifyProviders =
-          _debugCanModifyProviders;
-    }
-
-    _containerOf(widget).vsyncOverride = null;
-    assert(
-      _containerOf(newWidget).vsyncOverride == null,
-      'The ProviderContainer was already associated with a different widget',
-    );
-    _containerOf(newWidget).vsyncOverride = _flutterVsync;
-
-    super.update(newWidget);
-  }
-
   void _flutterVsync(void Function() task) {
     assert(_task == null, 'Only one task can be scheduled at a time');
     _task = task;
@@ -395,11 +376,14 @@ To fix this problem, you have one of two solutions:
   @override
   void unmount() {
     _mounted = false;
-    if (kDebugMode) {
-      _containerOf(widget).debugCanModifyProviders = null;
+    if (kDebugMode && debugCanModifyProviders == _debugCanModifyProviders) {
+      debugCanModifyProviders = null;
+    }
+
+    if (vsyncOverride == _flutterVsync) {
+      vsyncOverride = null;
     }
 
-    _containerOf(widget).vsyncOverride = null;
     super.unmount();
   }
 
diff --git a/packages/riverpod/CHANGELOG.md b/packages/riverpod/CHANGELOG.md
index 7a120e574e..df6688859c 100644
--- a/packages/riverpod/CHANGELOG.md
+++ b/packages/riverpod/CHANGELOG.md
@@ -19,6 +19,7 @@
   rebuilds from `ref.watch` vs rebuilds from `ref.refresh`.
 
 - Fixes a stackoverflow on Web caused by Dart (thanks to @leehack)
+- Fixes a bug when the root ProviderContainer is not associated with a ProviderScope.
 
 ## 2.0.2
 
diff --git a/packages/riverpod/lib/riverpod.dart b/packages/riverpod/lib/riverpod.dart
index f1d9f83224..b7cd54ae3b 100644
--- a/packages/riverpod/lib/riverpod.dart
+++ b/packages/riverpod/lib/riverpod.dart
@@ -16,6 +16,11 @@ export 'src/common.dart' hide AsyncTransition;
 
 export 'src/framework.dart'
     hide
+        debugCanModifyProviders,
+        vsync,
+        vsyncOverride,
+        ValueProviderElement,
+        ValueProvider,
         FamilyCreate,
         AsyncSelector,
         FamilyBase,
diff --git a/packages/riverpod/lib/src/framework.dart b/packages/riverpod/lib/src/framework.dart
index 842737547d..ee27a3ce96 100644
--- a/packages/riverpod/lib/src/framework.dart
+++ b/packages/riverpod/lib/src/framework.dart
@@ -22,3 +22,4 @@ part 'framework/proxy_provider_listenable.dart';
 part 'framework/ref.dart';
 part 'framework/selector.dart';
 part 'framework/scheduler.dart';
+part 'framework/value_provider.dart';
diff --git a/packages/riverpod/lib/src/framework/container.dart b/packages/riverpod/lib/src/framework/container.dart
index c77d6aa861..1f50ccf15d 100644
--- a/packages/riverpod/lib/src/framework/container.dart
+++ b/packages/riverpod/lib/src/framework/container.dart
@@ -2,10 +2,6 @@ part of '../framework.dart';
 
 ProviderBase? _circularDependencyLock;
 
-void _defaultVsync(void Function() task) {
-  Future(task);
-}
-
 int _debugNextId = 0;
 
 class _FamilyOverrideRef {
@@ -162,17 +158,21 @@ class ProviderContainer implements Node {
   /// A function that controls the refresh rate of providers.
   ///
   /// Defaults to refreshing providers at the end of the next event-loop.
+  @Deprecated('Will be removed in 3.0.0')
+  @internal
   void Function(void Function() task) get vsync {
     return vsyncOverride ?? _defaultVsync;
   }
 
   /// A way to override [vsync], used by Flutter to synchronize a container
   /// with the widget tree.
+  @Deprecated('Will be removed in 3.0.0')
+  @internal
   void Function(void Function() task)? vsyncOverride;
 
   /// The object that handles when providers are refreshed and disposed.
   late final _ProviderScheduler _scheduler =
-      _parent?._scheduler ?? _ProviderScheduler(vsync);
+      _parent?._scheduler ?? _ProviderScheduler();
 
   late final String _debugId;
 
@@ -223,6 +223,8 @@ class ProviderContainer implements Node {
   /// if it is safe to modify a provider.
   ///
   /// This corresponds to all the widgets that a [Provider] is associated with.
+  @Deprecated('Will be removed in 3.0.0')
+  @internal
   void Function()? debugCanModifyProviders;
 
   /// Whether [dispose] was called or not.
diff --git a/packages/riverpod/lib/src/framework/element.dart b/packages/riverpod/lib/src/framework/element.dart
index 63f6d73dd9..25077e0dbe 100644
--- a/packages/riverpod/lib/src/framework/element.dart
+++ b/packages/riverpod/lib/src/framework/element.dart
@@ -25,6 +25,13 @@ abstract class Refreshable<T> implements ProviderListenable<T> {
 abstract class AlwaysAliveRefreshable<T>
     implements Refreshable<T>, AlwaysAliveProviderListenable<T> {}
 
+/// A debug utility used by `flutter_riverpod`/`hooks_riverpod` to check
+/// if it is safe to modify a provider.
+///
+/// This corresponds to all the widgets that a [Provider] is associated with.
+@internal
+void Function()? debugCanModifyProviders;
+
 /// An internal class that handles the state of a provider.
 ///
 /// Do not use.
@@ -473,7 +480,7 @@ The provider ${_debugCurrentlyBuildingElement!.origin} modified $origin while bu
 ''',
         );
 
-        container.debugCanModifyProviders?.call();
+        debugCanModifyProviders?.call();
         return true;
       }(),
       '',
diff --git a/packages/riverpod/lib/src/framework/scheduler.dart b/packages/riverpod/lib/src/framework/scheduler.dart
index 874c037cda..009c644615 100644
--- a/packages/riverpod/lib/src/framework/scheduler.dart
+++ b/packages/riverpod/lib/src/framework/scheduler.dart
@@ -1,5 +1,22 @@
 part of '../framework.dart';
 
+/// A way to override [vsync], used by Flutter to synchronize a container
+/// with the widget tree.
+@internal
+void Function(void Function() task)? vsyncOverride;
+
+void _defaultVsync(void Function() task) {
+  Future(task);
+}
+
+/// A function that controls the refresh rate of providers.
+///
+/// Defaults to refreshing providers at the end of the next event-loop.
+@internal
+void Function(void Function()) get vsync {
+  return vsyncOverride ?? _defaultVsync;
+}
+
 /// The object that handles when providers are refreshed and disposed.
 ///
 /// Providers are typically refreshed at the end of the frame where they
@@ -7,10 +24,6 @@ part of '../framework.dart';
 ///
 /// Providers are disposed if they spent at least one full frame without any listener.
 class _ProviderScheduler {
-  _ProviderScheduler(this.vsync);
-
-  final void Function(void Function() onDone) vsync;
-
   final _stateToDispose = <AutoDisposeProviderElementMixin>[];
   final _stateToRefresh = <ProviderElementBase>[];
 
diff --git a/packages/riverpod/lib/src/value_provider.dart b/packages/riverpod/lib/src/framework/value_provider.dart
similarity index 76%
rename from packages/riverpod/lib/src/value_provider.dart
rename to packages/riverpod/lib/src/framework/value_provider.dart
index 901fd54080..cc29dfd71d 100644
--- a/packages/riverpod/lib/src/value_provider.dart
+++ b/packages/riverpod/lib/src/framework/value_provider.dart
@@ -1,7 +1,4 @@
-import 'package:meta/meta.dart';
-
-import 'framework.dart';
-import 'result.dart';
+part of '../framework.dart';
 
 /// A provider that is driven by a value instead of a function.
 ///
@@ -50,7 +47,25 @@ class ValueProviderElement<State> extends ProviderElementBase<State> {
     final previousState = getState()! as ResultData<State>;
 
     if (newValue != previousState.state) {
+      assert(
+        () {
+          // Asserts would otherwise prevent a provider rebuild from updating
+          // other providers
+          _debugSkipNotifyListenersAsserts = true;
+          return true;
+        }(),
+        '',
+      );
       setState(newValue);
+      assert(
+        () {
+          // Asserts would otherwise prevent a provider rebuild from updating
+          // other providers
+          _debugSkipNotifyListenersAsserts = false;
+          return true;
+        }(),
+        '',
+      );
       onChange?.call(newValue);
     }
   }
diff --git a/packages/riverpod/lib/src/internals.dart b/packages/riverpod/lib/src/internals.dart
index 59f757d107..f2e34fa88b 100644
--- a/packages/riverpod/lib/src/internals.dart
+++ b/packages/riverpod/lib/src/internals.dart
@@ -18,4 +18,3 @@ export 'run_guarded.dart';
 export 'state_notifier_provider.dart';
 export 'state_provider.dart';
 export 'stream_provider.dart';
-export 'value_provider.dart';


Test patch:
diff --git a/packages/flutter_riverpod/test/framework_test.dart b/packages/flutter_riverpod/test/framework_test.dart
index 31b0f52644..039b429b8a 100644
--- a/packages/flutter_riverpod/test/framework_test.dart
+++ b/packages/flutter_riverpod/test/framework_test.dart
@@ -1,7 +1,9 @@
+// ignore_for_file: invalid_use_of_internal_member
+
 import 'dart:async';
 
 import 'package:flutter/material.dart' hide Listener;
-import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_riverpod/src/internals.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mockito/mockito.dart';
 
@@ -242,13 +244,11 @@ void main() {
     expect(find.text('1'), findsOneWidget);
   });
 
-  testWidgets(
-      'UncontrolledProviderScope gracefully handles ProviderContainer.vsync',
+  testWidgets('UncontrolledProviderScope gracefully handles vsync',
       (tester) async {
     final container = createContainer();
-    final container2 = createContainer();
 
-    expect(container.vsyncOverride, null);
+    expect(vsyncOverride, null);
 
     await tester.pumpWidget(
       UncontrolledProviderScope(
@@ -257,31 +257,19 @@ void main() {
       ),
     );
 
-    expect(container.vsyncOverride, isNotNull);
-
-    await tester.pumpWidget(
-      UncontrolledProviderScope(
-        container: container2,
-        child: Container(),
-      ),
-    );
-
-    expect(container.vsyncOverride, null);
-    expect(container2.vsyncOverride, isNotNull);
+    expect(vsyncOverride, isNotNull);
 
     await tester.pumpWidget(Container());
 
-    expect(container.vsyncOverride, null);
-    expect(container2.vsyncOverride, null);
+    expect(vsyncOverride, null);
   });
 
   testWidgets(
-      'UncontrolledProviderScope gracefully handles ProviderContainer.debugCanModifyProviders',
+      'UncontrolledProviderScope gracefully handles debugCanModifyProviders',
       (tester) async {
     final container = createContainer();
-    final container2 = createContainer();
 
-    expect(container.debugCanModifyProviders, null);
+    expect(debugCanModifyProviders, null);
 
     await tester.pumpWidget(
       UncontrolledProviderScope(
@@ -290,22 +278,11 @@ void main() {
       ),
     );
 
-    expect(container.debugCanModifyProviders, isNotNull);
-
-    await tester.pumpWidget(
-      UncontrolledProviderScope(
-        container: container2,
-        child: Container(),
-      ),
-    );
-
-    expect(container.debugCanModifyProviders, null);
-    expect(container2.debugCanModifyProviders, isNotNull);
+    expect(debugCanModifyProviders, isNotNull);
 
     await tester.pumpWidget(Container());
 
-    expect(container.debugCanModifyProviders, null);
-    expect(container2.debugCanModifyProviders, null);
+    expect(debugCanModifyProviders, null);
   });
 
   testWidgets('ref.refresh forces a provider to refresh', (tester) async {