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.07k stars 175 forks source link

useState isnt triggerring rerenders when changed in a use effect #319

Closed saty9 closed 1 year ago

saty9 commented 1 year ago

Describe the bug useState seems to not trigger a rerender if given a new value in a useEffect

To Reproduce

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

Future<num> asyncSquare(int base) {
 return Future.delayed(const Duration(seconds: 1), () => pow(base, 2),);
}

AsyncSnapshot<num> useSquare(int base) {
  debugPrint("useSquare ran base: $base");
  final squareFuture = useState(asyncSquare(base));
  final result = useFuture(squareFuture.value);

  useEffect(() {
    debugPrint("effect triggered base: $base");
    final newFuture = asyncSquare(base);
    if  (newFuture != squareFuture.value) {
      debugPrint("new future doesnt have equality");
    }
    squareFuture.value = newFuture;

    return null;
  }, [base]);

  return result;
}

class Reproduction extends HookWidget {
  const Reproduction({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final squareInput = useState(2);
    final square = useSquare(squareInput.value);

    return Column(children: [Text("square: ${square.data}"), TextButton(onPressed: () {squareInput.value = 5;}, child: Text("change"))]);
  }
}

Expected behavior In the example I'd expect pressing the button to cause the text to change to 25 after a second

Actual behaviour It stays on 4

logs:

flutter: useSquare ran base: 2
flutter: effect triggered base: 2
flutter: new future doesnt have equality
flutter: useSquare ran base: 2
flutter: useSquare ran base: 2
flutter: useSquare ran base: 5
flutter: effect triggered base: 5
flutter: new future doesnt have equality
saty9 commented 1 year ago

made a reproduction not involving futures

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class Reproduction extends HookWidget {
  const Reproduction({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final squareInput = useState(2);
    final badlyMemoizedSquare = useState<num?>(null);
    final isEven = useState(false);

    useEffect(() {
      isEven.value = (badlyMemoizedSquare.value ?? 0) % 2 == 0;
      return null;
    }, [badlyMemoizedSquare.value]);

    useEffect(() {
      debugPrint("effect triggered base: ${squareInput.value}");
      badlyMemoizedSquare.value = pow(squareInput.value, 2);

      return null;
    }, [squareInput.value]);

    return Column(children: [Text("square: ${badlyMemoizedSquare.value} is ${isEven.value ? "even" : "odd"}"), TextButton(onPressed: () {squareInput.value += 1;}, child: Text("increase base"))]);
  }
}

also found a workaround for now:

Future.delayed(Duration.zero, () async {
      useStateResult.value = newFuture;
    });

scheduling the change to run like this means its executed after the current render which means the useState correctly causes a re-render

noga-dev commented 1 year ago

I assume it's because you'll need to mutate the useState value inside a WidgetsBinding.instance.addPostFrameCallback((timeStamp) { });

rrousselGit commented 1 year ago

made a reproduction not involving futures

What's supposed to happen? Why would your widget rebuild?

saty9 commented 1 year ago

The newer reproduction should count through square numbers and say if they are even or not instead it counts through square numbers and says if the last one was even or not. If this bug was in react you just wouldn't see the numbers change on the first button press as calling set state doesn't mutate the current state value but a value inside the hook and triggers a rerender. But this implementation you can mutate the result of the hook during a render which is why it doesn't show up in most cases

TimWhiting commented 1 year ago

I think the difference is in react useEffect schedules the function to be run after the frame whereas this implementation does it during the current frame.

saty9 commented 1 year ago

if the objective is a flutter implementation of react hooks should effects be changed to run after the frame like they would in react? I know that would probably be a breaking change

rrousselGit commented 1 year ago

useEffect in flutter_hooks is synchronous, to mimic how most Flutter life-cycles are synchronous too.

I have no plan to change this for now. You're free to wrap the state change in a Future(() => state.value = ... to work around this

mckuok commented 2 months ago

is it anti-pattern to update states inside useEffect?