rrousselGit / flutter_hooks

React hooks for Flutter. Hooks are a new kind of object that manages a Widget life-cycles. They are used to increase code sharing between widgets and as a complete replacement for StatefulWidget.
MIT License
3.12k stars 176 forks source link

useEffect hook not working properly. #153

Closed alirashid18 closed 2 years ago

alirashid18 commented 4 years ago

In React, useEffect hook works after first build. Bu in flutter_hooks, useEffect works before first render. That's why, make an operation with context object on useEffect(() { //operation using context }, []) makes following error: Cannot listen to inherited widgets inside HookState.initState. Use HookState.build instead.

REACT HOOKS WORKING EXAMPLE

Screen Shot 2020-07-12 at 13 58 40
gabrielvictorjs commented 4 years ago

In React, useEffect hook works after first build. Bu in flutter_hooks, useEffect works before first render. That's why, make an operation with context object on useEffect(() { //operation using context }, []) makes following error: Cannot listen to inherited widgets inside HookState.initState. Use HookState.build instead.

REACT HOOKS WORKING EXAMPLE

Screen Shot 2020-07-12 at 13 58 40

show your implementation with flutter_hooks

rrousselGit commented 4 years ago

You cannot perform operation with the context API inside useEffect in React either.

That doesn't work:

useEffect() => {
  useContext(MyContext); // not a valid usage
}, []);
rrousselGit commented 4 years ago

Although arguably, we could allow calling Provider's context.read inside useEffect(() {...}, [])

alirashid18 commented 4 years ago

@rrousselGit For example. I have config.json file in my assets folder. I need to read its data using DefaultAssetBundle. When I read data using loadString method (DefaultAssetBundle.of(context).loadString('assets/config.json')) it shows me an error. After this error, I copied useEffect hook source code and changed it initHook method as follows: void initHook() { super.initHook(); WidgetsBinding.instance.addPostFrameCallback((_) { scheduleEffect(); }); }

Then I used this hook and it worked as expectedly.

Screen Shot 2020-07-28 at 00 13 51 Screen Shot 2020-07-28 at 00 12 42
rrousselGit commented 4 years ago

Whatever.of is equivalent to useContext(Whatever). It is normal that this code is not allowed.

Instead do:

final bundle = DefaultAssetBundle.of(context);

useEffect(() {
  bundle.loadString('...').then(...);
}, [bundle]);
alirashid18 commented 4 years ago

It works, thanks @rrousselGit .

danielmahon commented 4 years ago

@rrousselGit What's the proper way to use a StateProvider inside useEffect without Future.microtask? Or is it required?

I get this error without it:

[VERBOSE-2:ui_dart_state.cc(171)] Unhandled Exception: setState() or markNeedsBuild() called during build.
This UncontrolledProviderScope 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:
  UncontrolledProviderScope
@freezed
abstract class AppState with _$AppState {
  const factory AppState({
    @Default(false) bool hasOnboarded,
    Widget fab,
    @Default([]) List<DepartmentModel> favorites,
  }) = _AppState;
}

final stateProvider = StateProvider((_) => const AppState());

final stateController = useProvider(stateProvider);

useEffect(() {
  // Future.microtask(() {
  stateController.state = stateController.state.copyWith(
    fab: FloatingActionButton(
      backgroundColor: theme.accentColor,
      onPressed: () {},
      child: const Icon(Icons.share),
    ),
  );
  // });
  return () {
    // Future.microtask(() {
    stateController.state = stateController.state.copyWith(fab: null);
    // });
  };
}, [stateController]);
rrousselGit commented 4 years ago

Or is it required

It is required. The exception was implemented on purpose to prevent modifying a provider inside build without the microtask because that is dangerous.

danielmahon commented 4 years ago

I figured, thanks. Pretty trivial to add my own useAsyncEffect. Loving riverpod + hooks by the way! Do you think it's worth revisiting adding a built-in useAsyncEffect to flutter_hooks?

void useAsyncEffect(
  FutureOr<dynamic> Function() effect,
  FutureOr<dynamic> Function() cleanup, [
  List<Object> keys,
]) {
  useEffect(() {
    Future.microtask(effect);
    return () {
      if (cleanup != null) {
        Future.microtask(cleanup);
      }
    };
  }, keys);
}
fdietze commented 3 years ago

This is my version of useAsyncEffect: (It tries to keep the signature similar to useEffect)

void useAsyncEffect(Future<Dispose?> Function() effect, [List<Object?>? keys]) {
  useEffect(() {
    final disposeFuture = Future.microtask(effect);
    return () => disposeFuture.then((dispose) => dispose?.call());
  }, keys);
}
rrousselGit commented 2 years ago

Closing since everything is working as expected.

I don't plan on adding a useAsyncEffect for now. You shouldn't need such a thing and it's likely a sign of code smell (such as modifying a state in the wrong place)

najibghadri commented 2 years ago

@rrousselGit Can you elaborate please why is this a sign of code smell?

I have many places in my code where a firs-time useEffect calls an async function.. some might use the context too. Why is this bad? 🙂

rrousselGit commented 2 years ago

Because if your problem is "can't call setState", the solution isn't to silence the error by using a hacky workaround of delaying the update by a frame

You should refactor your code such that the update is performed before all listeners of the object were built.

najibghadri commented 2 years ago

Oh okay, I thought calling async funcs in useEffect is considered bad in general.. 😅 Thanks @rrousselGit!

shtse8 commented 10 months ago

Because if your problem is "can't call setState", the solution isn't to silence the error by using a hacky workaround of delaying the update by a frame

You should refactor your code such that the update is performed before all listeners of the object were built.

Thanks for your explanation. Does it mean there is nothing wrong with useAsyncEffect? What if I want to do something in async while there is any changes on values? For example, in splash screen, there is a login process (async) when entering the page. after logging in successfully, user will be routed to another page. What is the correct implementation without useAsyncEffect?

Here is the code:

nextPage() {
      Navigator.of(context).push(
        MaterialPageRoute(
          maintainState: false,
          builder: (context) => LobbyPage(),
        ),
      );
    }

    // final acconutNotifier = ref.watch(accountProvider.notifier);
    final loginFuture = useMemoized(() => () async {
          controller.forward(); // animation
          final acconutNotifier = ref.read(accountProvider.notifier);
          await acconutNotifier.login();
          nextPage();
        }());
    useFuture(loginFuture);