flutter_hooks icon indicating copy to clipboard operation
flutter_hooks copied to clipboard

useState isnt triggerring rerenders when changed in a use effect

Open saty9 opened this issue 3 years ago • 6 comments

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 avatar Jul 21 '22 22:07 saty9

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

saty9 avatar Aug 20 '22 18:08 saty9

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

noga-dev avatar Aug 20 '22 19:08 noga-dev

made a reproduction not involving futures

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

rrousselGit avatar Aug 22 '22 09:08 rrousselGit

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

saty9 avatar Aug 22 '22 16:08 saty9

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.

TimWhiting avatar Sep 15 '22 04:09 TimWhiting

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

saty9 avatar Sep 18 '22 20:09 saty9

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

rrousselGit avatar Feb 19 '23 15:02 rrousselGit