Git Product home page Git Product logo

redux_saga's Introduction

Redux Saga Middleware Dart and Flutter

Dart CI

Issues related to redux_saga Pub Package Version Latest Dartdocs Join the chat on Gitter

Redux Saga Middleware for Dart and Flutter is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures.

The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects. Redux Saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well.

It uses synchronous generator functions to make those asynchronous flows easy to read, write and test. By doing so, these asynchronous flows look like your standard synchronous code. (kind of like async/await, but generators have a few more awesome features we need)

You might've used redux_thunk before to handle your data fetching. Contrary to redux thunk, you don't end up in callback hell, you can test your asynchronous flows easily and your actions stay pure.

Redux Saga is ported and compatible with javascript redux-saga implementation and its documentation. If you use Javascript redux-saga before than you can check Migration from Javascript documentation to get help about migration.

Package and installation details can be found at pub.dev.

Usage Example

Suppose we have a UI to fetch some user data from a remote server when a button is clicked. (For brevity, we'll just show the action triggering code.)

class MyComponent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ...
    RaisedButton(
      onPressed: () => StoreProvider.of(context).dispatch(UserFetchRequested()),
      child: Text('Fetch Users'),
    )
    ...
    );
  }
}

The Component dispatches a plain Object action to the Store. We'll create a Saga that watches for all UserFetchRequested actions and triggers an API call to fetch the user data.

sagas.dart

import 'package:redux_saga/redux_saga.dart';
import 'Api.dart';

// worker Saga: will be fired on UserFetchRequested actions
fetchUser({dynamic action}) sync* {
  yield Try(() sync* {
    var user = Result();
    yield Call(Api.fetchUser, args: [action.payload.userId], result: user);
    yield Put(UserFetchSuceeded(user: user.value));
  }, Catch: (e, s) sync* {
    yield Put(UserFetchFailed(message: e.message));
  });
}


//  Starts fetchUser on each dispatched `UserFetchRequested` action.
//  Allows concurrent fetches of user.
mySaga() sync* {
  yield TakeEvery(fetchUser, pattern: UserFetchRequested);
}


//  Alternatively you may use TakeLatest.
//
//  Does not allow concurrent fetches of user. If "UserFetchRequested" gets
//  dispatched while a fetch is already pending, that pending fetch is cancelled
//  and only the latest one will be run.
mySaga() sync* {
  yield TakeLatest(fetchUser, pattern: UserFetchRequested);
}

To run our Saga, we'll have to connect it to the Redux Store using the Redux Saga middleware.

main.dart

import 'package:redux/redux.dart';
import 'package:redux_saga/redux_saga.dart';

// create the saga middleware
var sagaMiddleware = createSagaMiddleware();

// Create store and apply middleware
final store = Store(
    counterReducer,
    initialState: 0,
    middleware: [applyMiddleware(sagaMiddleware)],
);

//connect to store
sagaMiddleware.setStore(store);

// then run the saga
sagaMiddleware.run(mySaga);

// render the application

Documentation

Examples

Vanilla Counter

Web based counter demo.

vanilla_counter

Counter

Demo using flutter and high-level API TakeEvery.

counter

Shopping Cart

A basic shopping cart example using flutter.

shopping_cart

Async App

A demo using async functions to fetch reddit posts.

async_app

License

Copyright (c) 2020 Bilal Uslu.

Licensed under The MIT License (MIT).

redux_saga's People

Contributors

bilal-uslu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

redux_saga's Issues

Debugging of saga!

Most of the time when I am trying to debug the saga, the cursor could not stop at the breakpoint. Is there anything I am missing?

Inheritance and type pattern matching

I have some abstract auth actions:

abstract class AuthStarted {
  const AuthStarted();
}

abstract class AuthStopped {
  final bool successful;

  const AuthStopped(this.successful);
}

And then I have extensions for cached and remote authentication methods:

class RemoteAuthStarted extends AuthStarted {
  const RemoteAuthStarted();
}

class RemoteAuthStopped extends AuthStopped {
  const RemoteAuthStopped(bool successful) : super(successful);
}

class StorageAuthStarted extends AuthStarted {
  const StorageAuthStarted();
}

class StorageAuthStopped extends AuthStopped {
  const StorageAuthStopped(bool successful) : super(successful);
}

Finally, I have these sagas to handle navigation:

Iterable<Effect> watchStorageAuthStopped() sync* {
  yield TakeEvery(navigateAfterAuthStopped, pattern: AuthStopped);
}

Iterable<Effect> navigateAfterAuthStopped({
  required AuthStopped action,
}) sync* {
  if (action.successful) {
    yield* _navigateTo(Route.collection);
  } else {
    if (action is StorageAuthStopped) {
      // This was an attempt to log in from storage, so no login was in storage.
      // Navigate to the login page.
      yield* _navigateTo(Route.collection);
    }
  }
}

I would expect my TakeEvery statement to run on all types of AuthStopped, but this isn't what happens. The _typeMatcher checks the runtimeType property, which doesn't take inheritence into account.

I propose we use is instead, as it seems like a better fit.

is there a way to return a value from a future function using Fork?

I have a function that returns Future<Map<String, SomeClass>> and I can call this with

yield Call(myFunction, args:[someArgs], result: myFuncRes)

but Call is blocking and I want to have this function be called with Fork - non blocking, but when I do this I get this error
type '_Task' is not a subtype of type Map<String, SomeClass>

flutter pub get is failing

[todo] flutter pub get
Running "flutter pub get" in todo...
Because every version of redux_saga depends on redux ^4.0.0 and todo depends on redux ^5.0.0, redux_saga is forbidden.

So, because todo depends on redux_saga ^2.0.2, version solving failed.
pub get failed (1; So, because todo depends on redux_saga ^2.0.2, version solving failed.)
exit code 1

When error happens, all other API request cease to work

I use redux_saga for my project and it is really a nice plugin.

Occasionally, I found when error happens (for example, due to calling setState for widget that no longer appears in the widget tree) during API call wrapped in saga, following up API request will all cease to work. When such an error happen, the only thing to get it back to normal is killing the app and launch it again. This is very bad for user experience.

As I pointed it out, reason of such errors is part of my coding issue. But I only expect it affect only that certain action. but no all other API calls. I'm not sure if there is a mechanism to prevent a certain failure from affecting the whole app (more accurately the saga system itself). Or maybe I don't use saga in a correct way. Please help. thanks.

`flutter: setState() called after dispose(): _CompetitionTeamDetailState#87ce4(lifecycle state: defunct, not mounted)
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
flutter: #0 State.setState. (package:flutter/src/widgets/framework.dart:1052:9)
#1 State.setState (package:flutter/src/widgets/framework.dart:1087:6)
#2 _CompetitionTeamDetailState.mappingTeamParticipantObject (package:MYPACKAGENAME/screen/CompetitionTeamDetail.dart:638:7)
#3 _CompetitionTeamDetailState.fetchTeamMember. (package:MYPACKAGENAME/screen/CompetitionTeamDetail.dart:745:9)
#4 fetchMyCompetitionTeamMemberDetail. (package:MYPACKAGENAME/redux/saga.dart:3631:16)
#5 _SyncIterator.moveNext (dart:core-patch/core_patch.dart:183:26)
#6 _taskRunner.next (package:redux_saga/src/taskRunner.dart:223:63)
#7 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#8 _taskRunner.digestEffect. (package:redux_saga/src/taskRunner.dart:346:16)
#9 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#10 Put._run. (package:redux_saga/src/effects/put.dart:59:12)
#11 _exec (package:redux_saga/src/scheduler.dart:28:9)
#12 _flush (package:redux_saga/src/scheduler.dart:70:5)
#13 asap (package:redux_saga/src/scheduler.dart:40:5)
#14 Put._run (package:redux_saga/src/effects/put.dart:41:5)
#15 _taskRunner.runEffect (package:redux_saga/src/taskRunner.dart:301:14)
#16 _taskRunner.digestEffect (package:redux_saga/src/taskRunner.dart:371:19)
#17 _taskRunner.next (package:redux_saga/src/taskRunner.dart:249:9)
#18 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#19 _taskRunner.digestEffect. (package:redux_saga/src/taskRunner.dart:346:16)
#20 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#21 _resolveFuture. (package:redux_saga/src/resolve.dart:4:37)
#22 _rootRunUnary (dart:async/zone.dart:1436:47)
#23 _CustomZone.runUnary (dart:async/zone.dart:1335:19)

flutter: 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.
flutter: #0 State.context. (package:flutter/src/widgets/framework.dart:909:9)
#1 State.context (package:flutter/src/widgets/framework.dart:915:6)
#2 _CompetitionTeamDetailState.fetchTeamMember. (package:MYPACKAGENAME/screen/CompetitionTeamDetail.dart:747:30)
#3 fetchMyCompetitionTeamMemberDetail. (package:MYPACKAGENAME/redux/saga.dart:3639:14)
#4 _SyncIterator.moveNext (dart:core-patch/core_patch.dart:183:26)
#5 _taskRunner.next (package:redux_saga/src/taskRunner.dart:223:63)
#6 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#7 _taskRunner.processFunctionReturn (package:redux_saga/src/taskRunner.dart:168:16)
#8 _taskRunner.switchToOnError (package:redux_saga/src/taskRunner.dart:134:7)
#9 _taskRunner.next (package:redux_saga/src/taskRunner.dart:263:9)
#10 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#11 _taskRunner.digestEffect. (package:redux_saga/src/taskRunner.dart:346:16)
#12 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#13 Put._run. (package:redux_saga/src/effects/put.dart:59:12)
#14 _exec (package:redux_saga/src/scheduler.dart:28:9)
#15 _flush (package:redux_saga/src/scheduler.dart:70:5)
#16 asap (package:redux_saga/src/scheduler.dart:40:5)
#17 Put._run (package:redux_saga/src/effects/put.dart:41:5)
#18 _taskRunner.runEffect (package:redux_saga/src/taskRunner.dart:301:14)
#19 _taskRunner.digestEffect (package:redux_saga/src/taskRunner.dart:371:19)
#20 _taskRunner.next (package:redux_saga/src/taskRunner.dart:249:9)
#21 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#22 _taskRunner.digestEffect. (package:redux_saga/src/taskRunner.dart:346:16)
#23 _TaskCallback.next (package:redux_saga/src/taskCallback.dart:18:16)
#24 _resolveFuture. (package:redux_saga/src/resolve.dart:4:37)
#25 _rootRunUnary (dart:async/zone.dart:1436:47)
#26 _CustomZone.runUnary (dart:async/zone.dart:1335:19)

#0 The above error occurred in task saga#484
#1 created by saga#482
#2 created by saga#244
#3 created by saga#243
#4 created by saga#1
#5 Tasks cancelled due to error:
#6 saga#244
#7 saga#3
#8 saga#6
#9 saga#9
#10 saga#12
#11 saga#15
#12 saga#18
#13 saga#21
#14 saga#24
#15 saga#27
#16 saga#30
#17 saga#33
#18 saga#36
#19 saga#39
#20 saga#42
#21 saga#45
#22 saga#48
#23 saga#51
#24 saga#54
#25 saga#57
#26 saga#60
#27 saga#63
#28 saga#66
#29 saga#69
#30 saga#72
#31 saga#75
#32 saga#78
#33 saga#81
#34 saga#84
#35 saga#87
#36 saga#90
#37 saga#93
#38 saga#96
#39 saga#99
#40 saga#102
#41 saga#105
#42 saga#108
#43 saga#111
#44 saga#114
#45 saga#117
#46 saga#120
#47 saga#123
#48 saga#126
#49 saga#129
#50 saga#132
#51 saga#135
#52 saga#138
#53 saga#141
#54 saga#144
#55 saga#147
#56 saga#150
#57 saga#153
#58 saga#156
#59 saga#159
#60 saga#162
#61 saga#165
#62 saga#168
#63 saga#171
#64 saga#174
#65 saga#177
#66 saga#180
#67 saga#183
#68 saga#186
#69 saga#189
#70 saga#192
#71 saga#195
#72 saga#198
#73 saga#201
#74 saga#204
#75 saga#207
#76 saga#210
#77 saga#213
#78 saga#216
#79 saga#219
#80 saga#222
#81 saga#225
#82 saga#228
#83 saga#231
#84 saga#234
#85 saga#237
#86 saga#240
#87 saga#246
#88 saga#249
#89 saga#252
#90 saga#255
#91 saga#258
#92 saga#261
#93 saga#264
#94 saga#267
#95 saga#270
#96 saga#273
#97 saga#276
#98 saga#279
#99 saga#282
#100 saga#285
#101 saga#288
#102 saga#291`

StoreConnector is being called multiple times even though there is no change in state

class ItemCreateScreen extends StatelessWidget {
  static final String route = '/item/create';
  ItemCreateScreen({Key key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, ItemCreateVM>(
      converter: (Store<AppState> store) {
        return ItemCreateVM.fromStore(store);
      },
      builder: (context, vm) {
        return ItemCreateForm(
          viewModel: vm,
        );
      },
    );
  }
}

TakeLeading stops listening actions after cancelling a saga task

I'm trying to cancel a saga task. The task cancellation works well but the problem is that no more saga tasks are triggered for the same action once a cancel occurs. Below is my cancel setup following Task cancellation docs.

  yield Try(() sync* {
      ...

  }, Catch: (e, s) sync* {
    logger.e(e);
  }, Finally: () sync* {
    // Do the needful cleanup if get turn task is cancelled by user
    var cancelled = Result<bool>();
    yield Cancelled(result: cancelled);
    if (cancelled.value!) {
      // Close the task as normal success operation
      yield Put(GetTurnSuccessAction());
      yield Put(ResetLoadingAction(key: loadingKey));
    }
  });

_watchGetTurn() sync* {
  var getTurnTask = Result<Task>();
  yield TakeLeading(_handleGetTurn, pattern: GetTurnAction, result: getTurnTask);

  // Listen to turn page close action.
  yield Take(pattern: PendingTurnClosedAction);

  // Cancel ongoing GetTurn saga
  yield Cancel([getTurnTask.value!]);
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.