brianegan / flutter_redux

A library that connects Widgets to a Redux Store
MIT License
1.65k stars 219 forks source link

Dispatching an action that does not alter state rerenders the widget, causing an infinite loop #243

Closed Lilja closed 1 year ago

Lilja commented 1 year ago

Very new to flutter.

I'm currently experiencing something which is weird in my book. I have a store that I'm connecting. Because of the lack of onMounted or equivalent in react(coming from vue) I would like to trigger a side effect once when the widget renders. To do this I tried to dispatch something and then catch is as a middleware. But something is not working I feel like.

My widget is essentially this:

return StoreConnector<RedditState, RedditVM>(
    builder: (context, redditVM) {
        redditVM.pingSubreddit(subreddit);
        // [...] omitted rest of component.
    }
}

Looking at pingSubreddit, you get this: store.dispatch(5). The reducer is checking the action type and performing logic accordingly. There is nothing that is supposed to happen when an integer is passed.

For some reason, this is rendering an infinite loop. I suppose that this package will always take a reducer and render the new components below it. Is this intended? I tried to replicate this using React's redux package by doing a dispatch during render. But it doesn't look like that forces an infinite loop.

Lilja commented 1 year ago

Example:

import 'package:flutter/material.dart';
import 'package:redux/redux.dart';

enum Actions { increment }

int counterReducer(int state, dynamic action) {
  return action == Actions.increment ? state + 1 : state;
}

void main() {
  runApp(StoreProvider<int>(
      child: MaterialApp(
          home: Scaffold(
              body: StoreConnector<int, VoidCallback>(
                  builder: (context, vm) {
                    print("render");
                    vm();
                    return const Text("data");
                  },
                  converter: (store) => () => store.dispatch("Foo")))),
      store: store));
}
Restarted application in 927ms.
2064 I/flutter (17550): render
Application finished.
Exited (sigterm)
brianegan commented 1 year ago

I believe you're looking for the onInit callback function which can be passed to the StoreConnector constructor.

https://pub.dev/documentation/flutter_redux/latest/flutter_redux/StoreConnector/onInit.html

If you dispatch an action inside the builder function, it will send that action to the Redux reducer, which triggers a state change, which triggers a rebuild, which runs the builder function, which dispatches again, which calls the recuer, produces a state changes, calls the builder... and you've found yourself in an infinite loop :)

Some sample code below (might be missing a paren somewhere -- github coding and didn't run it locally, but should get the idea across).

import 'package:flutter/material.dart';
import 'package:redux/redux.dart';

enum Actions { increment }

int counterReducer(int state, dynamic action) {
  return action == Actions.increment ? state + 1 : state;
}

void main() {
  runApp(StoreProvider<int>(
      store: store
      child: MaterialApp(
          home: Scaffold(
              body: StoreConnector<int, int>(
                  // Grab the current count from the store
                  converter: (store) => store.state,
                  // A function that runs once when the StoreConnector is inserted into the widget tree
                  onInit: (store) => store.dispatch(increment),
                  // Should be a pure function that returns a Widget tree and performs no side effects.
                  builder: (context, counter) {
                    return const Text("current count $counter");
                  },
        ),
      ),
  );
}
Lilja commented 1 year ago

Thanks @brianegan totally forgot onInit. It solves what i'm trying to do.

However, is it intended that when new state and old state is being reduced will still produce a rerender? Isn't that a bit wasteful?

brianegan commented 1 year ago

That's not quite how it works, perhaps my explanation was bad. The store only emits change events after the reducer produced a new state.

After you dispatch an action, the action goes through the middleware, then through the reducer, which produces a new state. After the new state is produced, the store emits a change event. The StoreConnector listens to those change events and runs the builder function to produce a new widget tree.

In Flutter, we do not have hooks, therefore builders / widget build methods are intended to be pure functions without side effects.

If you want to avoid a rebuild, you need to ensure the view model you emit implements the == method and pass the distinct: true option to the StoreConnector.

On Sat, Mar 4, 2023 at 02:12 Erik Lilja @.***> wrote:

Thanks @brianegan https://github.com/brianegan totally forgot onInit. It solves what i'm trying to do.

However, is it intended that when new state and old state is being reduced will still produce a rerender? Isn't that a bit wasteful?

— Reply to this email directly, view it on GitHub https://github.com/brianegan/flutter_redux/issues/243#issuecomment-1454672603, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAA65DA5LWZAZKGLDYHJ6HDW2MBQDANCNFSM6AAAAAAVPFOIZE . You are receiving this because you were mentioned.Message ID: @.***>

Lilja commented 1 year ago

Thanks! I'll have a look and reach out if I need more help. Much appreciated.