├── .gitignore ├── analysis_options.yaml ├── pubspec.yaml ├── CHANGELOG.md ├── .travis.yml ├── LICENSE ├── example └── example.dart ├── README.md ├── test └── redux_thunk_test.dart └── lib └── redux_thunk.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .packages 3 | packages 4 | pubspec.lock 5 | .dart_tool/ 6 | .pub 7 | doc/ 8 | coverage* 9 | *.iml 10 | lcov.info 11 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-casts: false 6 | implicit-dynamic: false 7 | 8 | linter: 9 | rules: 10 | - public_member_api_docs 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: redux_thunk 2 | description: A Redux.dart Middleware that allows you to dispatch functions that perform async work as actions. 3 | homepage: https://github.com/brianegan/redux_thunk 4 | version: 0.4.0 5 | 6 | environment: 7 | sdk: ">=2.12.0 <3.0.0" 8 | 9 | dependencies: 10 | redux: ">=5.0.0 <6.0.0" 11 | 12 | dev_dependencies: 13 | test: ^1.16.5 14 | pedantic: ^1.11.0 15 | 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.0 4 | 5 | * Breaking Change: updated to redux 5.0.0 6 | * Breaking Change: added null-safety 7 | 8 | ## 0.3.0 9 | 10 | * Breaking Change: Update to support Redux 4.x - 5.0 11 | * Thunks can now return values! 12 | 13 | ## 0.2.0 14 | 15 | * Update to work with Redux 3.0.0 & Dart 2 16 | 17 | ## 0.1.1 18 | 19 | * Move to github 20 | 21 | ## 0.1.0 22 | 23 | * Initial version, includes a `thunkMiddleware`, which intercepts and calls functions that are dispatched to the Store. 24 | 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | sudo: false 3 | dart: 4 | - stable 5 | with_content_shell: false 6 | dart_task: 7 | - dartanalyzer: --fatal-warnings lib 8 | - dartfmt 9 | script: 10 | - set -e 11 | - pub run test test/redux_thunk_test.dart 12 | - pub get 13 | - pub run test test/redux_thunk_test.dart --coverage=coverage 14 | - pub global activate coverage 15 | - pub global run coverage:format_coverage --in coverage/test/redux_thunk_test.dart.vm.json --out lcov.info --lcov 16 | after_success: 17 | - bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Brian Egan 3 | 4 | Permission is hereby granted, free of charge, to any 5 | person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the 7 | Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, 9 | sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall 14 | be included in all copies or substantial portions of 15 | the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 21 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 22 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 23 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:redux/redux.dart'; 4 | import 'package:redux_thunk/redux_thunk.dart'; 5 | 6 | void main() { 7 | // First, create a quick reducer 8 | String reducer(String state, dynamic action) => 9 | action is String ? action : state; 10 | 11 | // Next, apply the `thunkMiddleware` to the Store 12 | final store = Store( 13 | reducer, 14 | initialState: '', 15 | middleware: [thunkMiddleware], 16 | ); 17 | 18 | // Create a `ThunkAction`, which is any function that accepts the 19 | // Store as it's only argument. Our function (aka ThunkAction) will 20 | // simply send an action after 1 second. This is just an example, 21 | // but in real life, you could make a call to an HTTP service or 22 | // database instead! 23 | void action(Store store) async { 24 | var searchResults = await Future.delayed( 25 | Duration(seconds: 1), 26 | () => 'Search Results', 27 | ); 28 | 29 | store.dispatch(searchResults); 30 | } 31 | 32 | // Dispatch the action! The `thunkMiddleware` will intercept and invoke 33 | // the action function. 34 | store.dispatch(action); 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux_thunk 2 | 3 | [![Build Status](https://travis-ci.org/brianegan/redux_thunk.svg?branch=master)](https://travis-ci.org/brianegan/redux_thunk) [![codecov](https://codecov.io/gh/brianegan/redux_thunk/branch/master/graph/badge.svg)](https://codecov.io/gh/brianegan/redux_thunk) 4 | 5 | [Redux](https://pub.dartlang.org/packages/redux) provides a simple way to update a your application's State in response to synchronous Actions. However, it lacks tools to handle asynchronous code. This is where Thunks come in. 6 | 7 | The `thunkMiddleware` intercepts and calls `ThunkAction`s, which is simply a fancy name for any function that takes 1 argument: a Redux Store. This allows you to dispatch functions (aka `ThunkAction`s) to your Store that can perform asynchronous work, then dispatch actions using the Store after the work is complete. 8 | 9 | The dispatched `ThunkAction`s will be swallowed, meaning they will not go through the rest of your middleware to the `Store`'s `Reducer`. 10 | 11 | ### Example 12 | 13 | ```dart 14 | // First, create a quick reducer 15 | State reducer(String state, action) => 16 | action is String ? action : state; 17 | 18 | // Next, apply the `thunkMiddleware` to the Store 19 | final store = new Store( 20 | reducer, 21 | middleware: [thunkMiddleware], 22 | ); 23 | 24 | // Create a `ThunkAction`, which is any function that accepts the 25 | // Store as it's only argument. Our function (aka ThunkAction) will 26 | // simply send an action after 1 second. This is just an example, 27 | // but in real life, you could make a call to an HTTP service or 28 | // database instead! 29 | void action(Store store) async { 30 | final searchResults = await new Future.delayed( 31 | new Duration(seconds: 1), 32 | () => "Search Results", 33 | ); 34 | 35 | store.dispatch(searchResults); 36 | } 37 | 38 | // You can also use a function with some parameters to create a thunk, 39 | // and then use it like so: store.dispatch(waitAndDispatch(3)); 40 | ThunkAction waitAndDispatch(int secondsToWait) { 41 | return (Store store) async { 42 | final searchResults = await new Future.delayed( 43 | new Duration(seconds: secondsToWait), 44 | () => "Search Results", 45 | ); 46 | 47 | store.dispatch(searchResults); 48 | }; 49 | } 50 | 51 | 52 | ``` 53 | 54 | ## Credits 55 | 56 | All the ideas in this lib are shamelessly stolen from the original [redux-thunk](https://github.com/gaearon/redux-thunk) library and simply adapted to Dart. 57 | -------------------------------------------------------------------------------- /test/redux_thunk_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:redux/redux.dart'; 4 | import 'package:redux_thunk/redux_thunk.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | String identityReducer(String state, dynamic action) => 9 | action is String ? action : state; 10 | 11 | group('thunkMiddleware', () { 12 | test('intercepts and handles synchronous ThunkActions', () { 13 | final store = Store( 14 | identityReducer, 15 | initialState: '', 16 | middleware: [thunkMiddleware], 17 | ); 18 | void action(Store store) => store.dispatch('A'); 19 | 20 | store.dispatch(action); 21 | 22 | expect(store.state, 'A'); 23 | }); 24 | 25 | test('intercepts and handles synchronous CallableThunkActions', () { 26 | final store = Store( 27 | identityReducer, 28 | initialState: '', 29 | middleware: [thunkMiddleware], 30 | ); 31 | 32 | store.dispatch(SyncThunk()); 33 | 34 | expect(store.state, 'A'); 35 | }); 36 | 37 | test('dispatches an async action from ThunkActions', () async { 38 | final store = Store( 39 | identityReducer, 40 | initialState: '', 41 | middleware: [thunkMiddleware], 42 | ); 43 | Future action(Store store) async { 44 | final result = await Future.value('A'); 45 | 46 | store.dispatch(result); 47 | } 48 | 49 | await store.dispatch(action); 50 | 51 | expect(store.state, 'A'); 52 | }); 53 | 54 | test('dispatches an async action from CallableThunkActions', () async { 55 | final store = Store( 56 | identityReducer, 57 | initialState: '', 58 | middleware: [thunkMiddleware], 59 | ); 60 | 61 | await expectLater(store.dispatch(AsyncThunk()), completes); 62 | 63 | expect(store.state, 'A'); 64 | }); 65 | }); 66 | 67 | group('ExtraArgumentThunkMiddleware', () { 68 | test('handles sync ThunkActionWithExtraArgument', () { 69 | final store = Store( 70 | identityReducer, 71 | initialState: '', 72 | middleware: [ExtraArgumentThunkMiddleware(1)], 73 | ); 74 | void action(Store store, int arg) => store.dispatch('$arg'); 75 | 76 | store.dispatch(action); 77 | 78 | expect(store.state, '1'); 79 | }); 80 | 81 | test('handles sync CallableThunkActionWithExtraArgument', () { 82 | final store = Store( 83 | identityReducer, 84 | initialState: '', 85 | middleware: [ExtraArgumentThunkMiddleware(1)], 86 | ); 87 | 88 | store.dispatch(SyncExtra()); 89 | 90 | expect(store.state, '1'); 91 | }); 92 | 93 | test('handles async ThunkActionWithExtraArgument', () async { 94 | final store = Store( 95 | identityReducer, 96 | initialState: '', 97 | middleware: [ExtraArgumentThunkMiddleware(1)], 98 | ); 99 | Future action(Store store, int extra) async { 100 | await Future.value(); 101 | 102 | store.dispatch('$extra'); 103 | } 104 | 105 | await expectLater(store.dispatch(action), completes); 106 | 107 | expect(store.state, '1'); 108 | }); 109 | 110 | test('handles async CallableThunkActionWithExtraArgument', () async { 111 | final store = Store( 112 | identityReducer, 113 | initialState: '', 114 | middleware: [ExtraArgumentThunkMiddleware(1)], 115 | ); 116 | 117 | await store.dispatch(AsyncExtra()); 118 | 119 | expect(store.state, '1'); 120 | }); 121 | }); 122 | } 123 | 124 | class SyncThunk implements CallableThunkAction { 125 | @override 126 | void call(Store store) { 127 | store.dispatch('A'); 128 | } 129 | } 130 | 131 | class AsyncThunk implements CallableThunkAction { 132 | @override 133 | Future call(Store store) async { 134 | await Future.value('I'); 135 | store.dispatch('A'); 136 | } 137 | } 138 | 139 | class SyncExtra implements CallableThunkActionWithExtraArgument { 140 | @override 141 | void call(Store store, int extraArgument) { 142 | store.dispatch('$extraArgument'); 143 | } 144 | } 145 | 146 | class AsyncExtra implements CallableThunkActionWithExtraArgument { 147 | @override 148 | Future call(Store store, int extraArgument) async { 149 | await Future.value(); 150 | 151 | store.dispatch('$extraArgument'); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/redux_thunk.dart: -------------------------------------------------------------------------------- 1 | library redux_thunk; 2 | 3 | import 'package:redux/redux.dart'; 4 | 5 | /// The thunkMiddleware intercepts and calls [ThunkAction]s, which is simply a 6 | /// fancy name for any function that takes 1 argument: a Redux Store. This 7 | /// allows you to dispatch functions (aka [ThunkAction]s) to your Store that can 8 | /// perform asynchronous work, then dispatch actions using the Store after the 9 | /// work is complete. 10 | /// 11 | /// The dispatched [ThunkAction]s will be swallowed, meaning they will not go 12 | /// through the rest of your middleware to the [Store]'s [Reducer]. 13 | /// 14 | /// ### Example 15 | /// 16 | /// // First, create a quick reducer 17 | /// final reducer = (String state, action) => 18 | /// action is String ? action : state; 19 | /// 20 | /// // Next, apply the `thunkMiddleware` to the Store 21 | /// final store = new Store( 22 | /// reducer, 23 | /// middleware: [thunkMiddleware], 24 | /// ); 25 | /// 26 | /// // Create a `ThunkAction`, which is any function that accepts the 27 | /// // Store as it's only argument. Our function (aka ThunkAction) will 28 | /// // simply send an action after 1 second. This is just an example, 29 | /// // but in real life, you could make a call to an HTTP service or 30 | /// // database instead! 31 | /// final action = (Store store) async { 32 | /// final searchResults = await new Future.delayed( 33 | /// new Duration(seconds: 1), 34 | /// () => "Search Results", 35 | /// ); 36 | /// 37 | /// store.dispatch(searchResults); 38 | /// }; 39 | dynamic thunkMiddleware( 40 | Store store, 41 | dynamic action, 42 | NextDispatcher next, 43 | ) { 44 | if (action is ThunkAction) { 45 | return action(store); 46 | } else if (action is CallableThunkAction) { 47 | return action.call(store); 48 | } else { 49 | return next(action); 50 | } 51 | } 52 | 53 | /// The [ExtraArgumentThunkMiddleware] works exactly like the normal 54 | /// [thunkMiddleware] with one difference: It injects the provided "extra 55 | /// argument" into all Thunk functions. 56 | /// 57 | /// ### Example 58 | /// 59 | /// ```dart 60 | /// // First, create a quick reducer 61 | /// final reducer = (String state, action) => action is String ? action : state; 62 | /// 63 | /// // Next, apply the `ExtraArgumentThunkMiddleware` to the Store. In this 64 | /// // case, we want to provide an http client to each thunk function. 65 | /// final store = new Store( 66 | /// reducer, 67 | /// middleware: [ExtraArgumentThunkMiddleware(http.Client())], 68 | /// ); 69 | /// 70 | /// // Create a `ThunkActionWithExtraArgument`, which is a fancy name for a 71 | /// // function that takes in a Store and the extra argument provided above 72 | /// // (the http.Client). 73 | /// Future fetchBlogAction(Store store, http.Client client) async { 74 | /// final response = await client.get('https://jsonplaceholder.typicode.com/posts'); 75 | /// 76 | /// store.dispatch(response.body); 77 | /// } 78 | /// ``` 79 | class ExtraArgumentThunkMiddleware extends MiddlewareClass { 80 | /// An Extra argument that will be injected into every thunk function. 81 | final A extraArgument; 82 | 83 | /// Create a ThunkMiddleware that will inject an extra argument into every 84 | /// thunk function 85 | ExtraArgumentThunkMiddleware(this.extraArgument); 86 | 87 | @override 88 | dynamic call(Store store, dynamic action, NextDispatcher next) { 89 | if (action is ThunkActionWithExtraArgument) { 90 | return action(store, extraArgument); 91 | } else if (action is CallableThunkActionWithExtraArgument) { 92 | return action.call(store, extraArgument); 93 | } else { 94 | return next(action); 95 | } 96 | } 97 | } 98 | 99 | /// A function that can be dispatched as an action to a Redux [Store] and 100 | /// intercepted by the the [thunkMiddleware]. It can be used to delay the 101 | /// dispatch of an action, or to dispatch only if a certain condition is met. 102 | /// 103 | /// The ThunkFunction receives a [Store], which it can use to get the latest 104 | /// state if need be, or dispatch actions at the appropriate time. 105 | typedef ThunkAction = dynamic Function(Store store); 106 | 107 | /// An interface that can be implemented by end-users to create a class-based 108 | /// [ThunkAction]. 109 | abstract class CallableThunkAction { 110 | /// The method that acts as the [ThunkAction] 111 | dynamic call(Store store); 112 | } 113 | 114 | /// A function that can be dispatched as an action to a Redux [Store] and 115 | /// intercepted by the the [ExtraArgumentThunkMiddleware]. It can be used to 116 | /// delay the dispatch of an action, or to dispatch only if a certain condition 117 | /// is met. 118 | /// 119 | /// The [Store] argument is used to get the latest state if need be, or dispatch 120 | /// actions at the appropriate time. 121 | /// 122 | /// The [extraArgument] argument is injected via [ExtraArgumentThunkMiddleware]. 123 | typedef ThunkActionWithExtraArgument = dynamic Function( 124 | Store store, 125 | A extraArgument, 126 | ); 127 | 128 | /// An interface that can be implemented by end-users to create a class-based 129 | /// [ThunkActionWithExtraArgument]. 130 | abstract class CallableThunkActionWithExtraArgument { 131 | /// The method that acts as the [ThunkActionWithExtraArgument] 132 | dynamic call(Store store, A extraArgument); 133 | } 134 | --------------------------------------------------------------------------------