signals.dart icon indicating copy to clipboard operation
signals.dart copied to clipboard

FlutterError (This widget has been unmounted, so the State no longer has a context (and should be considered defunct). Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.)

Open Yanren1225 opened this issue 8 months ago • 8 comments

I wrote a simple component like this:

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const HomeView(),
    );
  }
}

final data = signal(0);

void increment() {
  data.value++;
}

class HomeView extends StatelessWidget {
  const HomeView({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Demo')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Watch((_) => Text(data.value.toString())),
          const SizedBox(height: 20),
          ElevatedButton(onPressed: increment, child: const Text('Increment')),
        ],
      ),
    );
  }
}

Then I edited this part:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Watch((_) => Text(data.value.toString())),
            const SizedBox(height: 20),
            ElevatedButton(onPressed: increment, child: const Text('Increment')),
          ],
        ),
      ),
    );
  }

I added a Center widget, and then I got an error:

FlutterError (This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.)

Why is this happening? Did I write something wrong?

Yanren1225 avatar Apr 07 '25 15:04 Yanren1225

I tried using a StatefulWidget, but it still doesn't work:

class HomeView extends StatefulWidget {
  const HomeView({super.key});

  @override
  State<HomeView> createState() => _HomeViewState();
}

class _HomeViewState extends State<HomeView> {
  final data = signal(0);

  void increment() {
    data.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Watch((_) => Text(data.value.toString())),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: increment,
              child: const Text('Increment'),
            ),
          ],
        ),
      ),
    );
  }
}

It only works correctly in a StatelessWidget for hot reload:

class HomeView extends StatelessWidget {
  HomeView({super.key});

  final data = signal(0);

  void increment() {
    data.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Demo')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Watch((_) => Text(data.value.toString())),
          const SizedBox(height: 20),
          ElevatedButton(
            onPressed: increment,
            child: const Text('Increment'),
          ),
        ],
      ),
    );
  }
}

Yanren1225 avatar Apr 07 '25 16:04 Yanren1225

Image In the official MVI demo, adding a Center or other Widget at this position also triggers this error Image

Yanren1225 avatar Apr 07 '25 17:04 Yanren1225

Does a hot restart fix it?

rodydavis avatar Apr 07 '25 17:04 rodydavis

Does a hot restart fix it?

There is no problem with Hot restart, but the problem is with Hot reload, and Hot reload is something that is often used in development.

Yanren1225 avatar Apr 08 '25 01:04 Yanren1225

I think I've experienced this issue a couple times today.

Hackmodford avatar Apr 17 '25 17:04 Hackmodford

Hot reload is a known issue and working on seeing if there is a way to address it.

The issue is about resubscribing all the listeners and when hot reload happens you either get widgets that do not update if the signal changes post reload or what we have now.

rodydavis avatar Apr 22 '25 16:04 rodydavis

That is also why I recommend the SignalsMixin

rodydavis avatar Apr 22 '25 16:04 rodydavis

Hello everyone. I think I tracked down and found a possible fix.

This bug is closely related to #398. In lib/src/watch/builder.dart. If the Watcher widgets gets replaced in the widget tree with a new instance on hot reload, the _WatchState.result gets disposed. The post-frame-callback still runs and tries to calculate the disposed result which throws. Adding a guard clause seems to solve the problem:

void reassemble() {
    super.reassemble();
    final target = core.SignalsObserver.instance;
    if (target is core.DevToolsSignalsObserver) {
      target.reassemble();
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
+     if (result.disposed) return;
      result.recompute();
      if (mounted) setState(() {});
      result.value;
    });
  }

Can you confirm?

raphaelporsche avatar Oct 15 '25 08:10 raphaelporsche