redux_epics 0.12.0
Redux Epics 
Redux is great for synchronous updates to a store in response to actions. However, it does not have any built-in mechanisms for asynchronous operations, such as making an api call or retrieving information from a database in response to an action. This is where Epics come in!
The best part: Epics are based on Dart Streams. This makes routine tasks easy, and complex tasks such as asynchronous error handling, cancellation, and debouncing a breeze.
Note: For users unfamiliar with Streams, simple async cases are easier to handle with a normal Middleware Function. If normal Middleware Functions, Thunks, or Futures work for you, you're doing it right! When you find yourself dealing with more complex scenarios, such as writing an Autocomplete UI, check out the Recipes below to see how Streams / Epics can make your life easier.
Dart 2 #
- Version 0.7.x supports Dart 1
- Version 0.8.0 and up supports Dart 2
Example #
Let's say your app has a search box. When a user submits a search term, you dispatch a PerformSearchAction
which contains the term. In order to actually listen for the PerformSearchAction
and make a network request for the results, we can create an Epic!
In this instance, our Epic will need to filter all incoming actions it receives to only the Action
it is interested in: the PerformSearchAction
. This will be done using the where
method on Streams. Then, we need to make a network request with the search term using asyncMap
method. Finally, we need to transform those results into an action that contains the search results. If an error has occurred, we'll want to return an error action so our app can respond accordingly.
Here's what the above description looks like in code.
import 'dart:async';
import 'package:redux_epics/redux_epics.dart';
Stream<dynamic> exampleEpic(Stream<dynamic> actions, EpicStore<State> store) {
return actions
.where((action) => action is PerformSearchAction)
.asyncMap((action) =>
// Pseudo api that returns a Future of SearchResults
api.search((action as PerformSearch).searchTerm)
.then((results) => new SearchResultsAction(results))
.catchError((error) => new SearchErrorAction(error)));
}
Connecting the Epic to the Redux Store #
Now that we've got an epic to work with, we need to wire it up to our Redux store so it can receive a stream of actions. In order to do this, we'll employ the EpicMiddleware
.
import 'package:redux_epics/redux_epics.dart';
import 'package:redux/redux.dart';
var epicMiddleware = new EpicMiddleware(exampleEpic);
var store = new Store<State>(fakeReducer, middleware: [epicMiddleware]);
Combining epics and normal middleware #
To combine the epic Middleware and normal middleware, simply use both in the list! Note: You may need to provide
var store = new Store<AppState>(
fakeReducer,
middleware: [myMiddleware, EpicMiddleware<AppState>(exampleEpic)],
);
If you're combining two Lists, please make sure to use the +
or the addAll
method with the cascade operator (myList..addAll
).
var store = new Store<AppState>(
fakeReducer,
middleware: [myMiddleware] + [EpicMiddleware<AppState>(exampleEpic)],
);
Combining Epics #
Rather than having one massive Epic that handles every possible type of action, it's best to break Epics down into smaller, more manageable and testable units. This way we could have a searchEpic
, a chatEpic
, and an updateProfileEpic
, for example.
However, the EpicMiddleware
accepts only one Epic. So what are we to do? Fear not: redux_epics includes class for combining Epics together!
import 'package:redux_epics/redux_epics.dart';
final epic = combineEpics<State>([
searchEpic,
chatEpic,
updateProfileEpic,
]);
Advanced Recipes #
In order to perform more advanced operations, it's often helpful to use a library such as RxDart.
Casting #
In order to use this library effectively, you generally need filter down to actions of a certain type, such as PerformSearchAction
. In the previous examples, you'll noticed that we need to filter using the where
method on the Stream, and then manually cast (action as SomeType
) later on.
To more conveniently narrow down actions to those of a certain type, you have two options:
TypedEpic #
The first option is to use the built-in TypedEpic
class. This will allow you to write Epic functions that handle actions of a specific type, rather than all actions!
final epic = new TypedEpic<State, PerformSearchAction>(searchEpic);
Stream<dynamic> searchEpic(
// Note: This epic only handles PerformSearchActions
Stream<PerformSearchAction> actions,
EpicStore<State> store,
) {
return actions
.asyncMap((action) =>
// No need to cast the action to extract the search term!
api.search(action.searchTerm)
.then((results) => new SearchResultsAction(results))
.catchError((error) => new SearchErrorAction(error)));
}
RxDart #
You can use the ofType
method provided by RxDart. It will both perform a where
check and then cast the action for you.
import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';
Stream<dynamic> ofTypeEpic(Stream<dynamic> actions, EpicStore<State> store) {
// Wrap our actions Stream as an Observable. This will enhance the stream with
// a bit of extra functionality.
return new Observable(actions)
// Use `ofType` to narrow down to PerformSearchAction
.ofType(new TypeToken<PerformSearchAction>())
.asyncMap((action) =>
// No need to cast the action to extract the search term!
api.search(action.searchTerm)
.then((results) => new SearchResultsAction(results))
.catchError((error) => new SearchErrorAction(error)));
}
Cancellation #
In certain cases, you may need to cancel an asynchronous task. For example, your app begins loading data in response to a user clicking on a the search button by dispatching a PerformSearchAction
, and then the user hit's the back button in order to correct the search term. In that case, your app dispatches a CancelSearchAction
. We want our Epic
to cancel the previous search in response to the action. So how can we accomplish this?
This is where Observables really shine. In the following example, we'll employ Observables from the RxDart library to beef up the power of streams a bit, using the flatMapLatest
and takeUntil
operator.
import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';
Stream<dynamic> cancelableSearchEpic(
Stream<dynamic> actions,
EpicStore<State> store,
) {
return new Observable(actions)
.ofType(new TypeToken<PerformSearchAction>())
// Use FlatMapLatest. This will ensure if a new PerformSearchAction
// is dispatched, the previous searchResults will be automatically
// discarded.
//
// This prevents your app from showing stale results.
.flatMapLatest((action) {
return new Observable.fromFuture(api
.search(action.searchTerm)
.then((results) => new SearchResultsAction(results))
.catchError((error) => new SearchErrorAction(error)))
// Use takeUntil. This will cancel the search in response to our
// app dispatching a `CancelSearchAction`.
.takeUntil(actions.where((action) => action is CancelSearchAction));
});
}
Autocomplete using debounce #
Let's take this one step further! Say we want to turn our previous example into an Autocomplete Epic. In this case, every time the user types a letter into the Text Input, we want to fetch and show the search results. Each time the user types a letter, we'll dispatch a PerformSearchAction
.
In order to prevent making too many API calls, which can cause unnecessary load on your backend servers, we don't want to make an API call on every single PerformSearchAction
. Instead, we'll wait until the user pauses typing for a short time before calling the backend API.
We'll achieve this using the debounce
operator from RxDart.
import 'package:redux_epics/redux_epics.dart';
import 'package:rxdart/rxdart.dart';
Stream<dynamic> autocompleteEpic(
Stream<dynamic> actions,
EpicStore<State> store,
) {
return new Observable(actions)
.ofType(new TypeToken<PerformSearchAction>())
// Using debounce will ensure we wait for the user to pause for
// 150 milliseconds before making the API call
.debounce(new Duration(milliseconds: 150))
.flatMapLatest((action) {
return new Observable.fromFuture(api
.search(action.searchTerm)
.then((results) => new SearchResultsAction(results))
.catchError((error) => new SearchErrorAction(error)))
.takeUntil(actions.where((action) => action is CancelSearchAction));
});
}
Dependency Injection #
As of March 2018 there isn't a production-ready DI solution for Flutter. In the meantime depenendencies can be injected manually with either a Functional or an Object-Oriented style.
Functional #
// epic_file.dart
Epic<AppState> createEpic(WebService service) {
return (Stream<dynamic> actions, EpicStore<AppState> store) async* {
service.doSomething()...
}
}
OO #
// epic_file.dart
class MyEpic implements EpicClass<State> {
final WebService service;
MyEpic(this.service);
@override
Stream<dynamic> call(Stream<dynamic> actions, EpicStore<State> store) {
service.doSomething()...
}
}
Usage - Production #
In production code the epics can be created at the point where combineEpics
is called. If you're using separate main_<environment>.dart
files to configure your application for different environments you may want to pass the config to the RealWebService
at this point.
// app_store.dart
import 'package:epic_file.dart';
...
final apiBaseUrl = config.apiBaseUrl
final functionalEpic = createEpic(new RealWebService(apiBaseUrl));
// or
final ooEpic = new MyEpic(new RealWebService(apiBaseUrl));
static final epics = combineEpics<AppState>([
functionalEpic,
ooEpic,
...
]);
static final epicMiddleware = new EpicMiddleware(epics);
Usage - Testing #
...
final testFunctionalEpic = createEpic(new MockWebService());
// or
final testOOEpic = new MyEpic(new MockWebService());
...
0.12.0 #
- Breaking Change: Support for Redux 4.x - 5.0
0.11.0 #
- Bugfix: Initializing Combined Epics is performed only once. Combined Epics were previously initialized twice. Thanks @solid-software!
0.10.6 #
- Bump RxDart version compatibility to latest version: 0.22.x
0.10.5 #
- Bump RxDart version compatibility to latest version: 0.21.x
0.10.4 #
- Add
onChange
to the EpicStore - Bugfix for Dart 2.0.0 - import missing
dart:async
0.10.3 #
- Fixes pana issues:
- Provide longer description
- Provide example file
0.10.2 #
- Support latest version of RxDart (0.20.x) (thanks @MichaelMarner!)
- Add
onChange
stream to EpicStore (thanks @jnorkus) - Add docs for combining EpicMiddleware with other Middleware (thanks @Henge9!)
0.10.1 #
- Fix TypedEpic when not supporting async generators
0.10.2 #
- Add option to async* functions for performance reasons. This option will be removed in the future when Dart supports running async* functions synchronously (https://github.com/dart-lang/sdk/issues/33818)
0.10.0 #
- Updated to work with latest version of RxDart, which removes deprecated Stream methods in Dart 2
0.9.0 #
- Now works with Redux 3.0.0 & RxDart 0.16.5, which have been upgraded to work with Dart 2
0.8.0 #
- Breaking Change: Dart 2 Support, Dart 1 supported by 0.7.x
- Upgrade to RxDart 0.16
0.7.0 #
- Breaking Change: Actions you emit from your Epic are now re-dispatched through all Epics. They used to be simply forwarded to the next Middleware in the chain.
- Added support for
async*
functions - Added
TypedEpic
as a convenient way to bind actions of a certain type to an epics.
0.6.1 #
Improve docs, bump to ensure it works with latest RxDart.
0.6.0 #
- Breaking Api Changes
- Updated to work with Redux 2.0.0
Epic
is now atypedef
CombinedEpic
is nowcombineEpics
import 'package:redux/redux.dart';
import 'package:redux_epics/redux_epics.dart';
enum Actions { increment, decrement }
int reducer(int prev, dynamic action) {
if (action == Actions.increment) {
return prev + 1;
} else if (action == Actions.decrement) {
return prev - 1;
}
return prev;
}
// A middleware that will listen for increment actions and then undo them with
// a decrement action! What a joker!
//
// We use an async* function here to make life easier! You can also return a
// normal Streams, or use RxDart to enhance the power of Streams!
Stream<dynamic> jokerEpic(
Stream<dynamic> actions,
EpicStore<int> store,
) async* {
// Use the `await for` keyword to listen to the stream inside an async*
// function!
await for (var action in actions) {
// Then check to see if we've received an increment action
if (action == Actions.increment) {
// If so, emit a decrement action to the Stream using the `yield` keyword!
// This decrement action will be automatically dispatched.
yield Actions.decrement;
}
}
}
void main() {
final store = Store<int>(
reducer,
initialState: 0,
middleware: [EpicMiddleware(jokerEpic)],
);
store.onChange.listen(print);
store.dispatch(Actions.increment);
}
Use this package as a library
1. Depend on it
Add this to your package's pubspec.yaml file:
dependencies:
redux_epics: ^0.12.0
2. Install it
You can install packages from the command line:
with pub:
$ pub get
with Flutter:
$ flutter pub get
Alternatively, your editor might support pub get
or flutter pub get
.
Check the docs for your editor to learn more.
3. Import it
Now in your Dart code, you can use:
import 'package:redux_epics/redux_epics.dart';
Popularity:
Describes how popular the package is relative to other packages.
[more]
|
92
|
Health:
Code health derived from static analysis.
[more]
|
97
|
Maintenance:
Reflects how tidy and up-to-date the package is.
[more]
|
90
|
Overall:
Weighted score of the above.
[more]
|
93
|
We analyzed this package on Dec 13, 2019, and provided a score, details, and suggestions below. Analysis was completed with status completed using:
- Dart: 2.7.0
- pana: 0.13.1+4
Health suggestions
Fix lib/src/epic_middleware.dart
. (-2.48 points)
Analysis of lib/src/epic_middleware.dart
reported 5 hints:
line 22 col 7: Unnecessary new keyword.
line 24 col 7: Unnecessary new keyword.
line 38 col 15: Unnecessary new keyword.
line 39 col 51: Unnecessary new keyword.
line 53 col 7: Unnecessary new keyword.
Fix lib/src/combine_epics.dart
. (-0.50 points)
Analysis of lib/src/combine_epics.dart
reported 1 hint:
line 27 col 12: Unnecessary new keyword.
Fix lib/src/epic.dart
. (-0.50 points)
Analysis of lib/src/epic.dart
reported 1 hint:
line 119 col 25: Unnecessary new keyword.
Maintenance issues and suggestions
Support latest dependencies. (-10 points)
The version constraint in pubspec.yaml
does not support the latest published versions for 1 dependency (rxdart
).