redux-await
Manage async redux actions sanely
Breaking Changes!!
redux-await
now takes control of a branch of your state/reducer tree similar to redux-form
, and also like redux-form
you need to use this module's version of connect
and not react-redux
's
Install
npm install --save redux-await
Usage
This module exposes a middleware, reducer, and connector to take care of async state in a redux app. You'll need to:
-
Apply the middleware:
;let createStoreWithMiddleware =createStore; -
Install the reducer into the
await
path of yourcombineReducers
;// old code// const store = applyMiddleware(thunk)(createStore)(reducers);// new code;const store = createStore...reducersawait: awaitReducer; -
Use the
connect
function from this module and notreact-redux
's// old code// import { connect } from 'react-redux';// new code;{ /* ... */ }statefooFooPage
Now your action payloads can contain promises, you just need to add AWAIT_MARKER
to the
action like this:
// old code//export const getTodos = () => ({// type: GET_TODOS,// payload: {// loadedTodos: localStorage.todos,// },//});//export const addTodo = todo => ({// type: ADD_TODO,// payload: {// savedTodo: todo,// },//}); // new code;const getTodos = type: GET_TODOS AWAIT_MARKER payload: loadedTodos: api // returns promise ;const addTodo = type: ADD_TODO AWAIT_MARKER payload: savedTodo: api // returns promise ;
Now your containers barely need to change:
{ const todos statuses errors = thisprops; // old code //return <div> // <MyList data={todos} /> //</div>; // new code return <div> statusesloadedTodos === 'pending' && <div>Loading...</div> statusesloadedTodos === 'success' && <MyList data=loadedTodos /> statusesloadedTodosstatus === 'failure' && <div>Oops: errorsloadedTodosmessage</div> statusessavedTodo === 'pending' && <div>Saving </div> statusessavedTodo === 'failure' && <div>There was an error saving</div> </div>; } //old code// import { connect } from 'react-redux'; // new code; // it just spreads state.await on props statetodosContainer
Why
Redux is mostly concerned about how to manage state in a synchronous setting. Async apps create challenges like keeping track of the async status and dealing with async errors. While it is possible to build an app this way using redux-thunk and/or redux-promise it tends to bloat the app and it makes unit testing needlessly verbose
redux-await
tries to solve all of these problems by keeping track of async payloads by means
of a middleware and a reducer keeping track of payload properties statuses. Let's walk
through the development of a TODO app (App 1) that starts without any async and then needs to
start converting action from sync to async. We'll first try only using redux-thunk
to solve
this (App 2), and then see how to solve this with redux-await
(App 3)
For the first version of the app we're going to store the todos in localStorage. Here's a simple way we would do it:
App1 demo
App 1
;;;;;; const GET_TODOS = 'GET_TODOS';const ADD_TODO = 'ADD_TODO';const SAVE_APP = 'SAVE_APP';const actions = { const todos = JSON; return type: GET_TODOS payload: todos ; } { return type: ADD_TODO payload: todo ; } { return { localStoragetodos = JSON; ; } };const initialState = isAppSynced: false todos: ;const todosReducer = { if actiontype === GET_TODOS return ...state isAppSynced: true todos: actionpayloadtodos ; if actiontype === ADD_TODO return ...state isAppSynced: false todos: statetodos ; if actiontype === SAVE_APP return ...state isAppSynced: true ; return state;};const reducer = const store = createStorereducer; { thisprops; } { const dispatch todos isAppSynced = thisprops; const input = thisrefs; return <div> isAppSynced && 'app is synced up' <ul>todos</ul> <input ref="input" type="text" onBlur= /> <button onClick= >Sync</button> <br /> <pre>JSON</pre> </div>; }const ConnectedApp = App; ReactDOM;
Looks cool (it's a POC so it's purposely minimal), but let's say you want to start using an API which is async to store the state, now your app will look something like App 2:
App2 demo
App 2
;;;;;; // this not an API, this is a tributeconst api = { return { ; }; } { return { ; }; } const GET_TODOS_PENDING = 'GET_TODOS_PENDING';const GET_TODOS = 'GET_TODOS';const GET_TODOS_ERROR = 'GET_TODOS_ERROR';const ADD_TODO = 'ADD_TODO';const SAVE_APP_PENDING = 'SAVE_APP_PENDING'const SAVE_APP = 'SAVE_APP';const SAVE_APP_ERROR = 'SAVE_APP_ERROR';const actions = { return { ; api ; ; } } { return type: ADD_TODO payload: todo ; } { return { ; api ; } };const initialState = isAppSynced: false isFetching: false fetchingError: null isSaving: false savingError: null todos: ;const todosReducer = { if actiontype === GET_TODOS_PENDING return ...state isFetching: true fetchingError: null ; if actiontype === GET_TODOS return ...state isAppSynced: true isFetching: false fetchingError: null todos: actionpayloadtodos ; if actiontype === GET_TODOS_ERROR return ...state isFetching: false fetchingError: actionpayloadmessage ; if actiontype === ADD_TODO return ...state isAppSynced: false todos: statetodos ; if actiontype === SAVE_APP_PENDING return ...state isSaving: true savingError: null ; if actiontype === SAVE_APP return ...state isAppSynced: true isSaving: false savingError: null ; if action === SAVE_APP_ERROR return ...state isSaving: false savingError: actionpayloadmessage return state;};const reducer = const store = createStorereducer; { thisprops; } { const dispatch todos isAppSynced isFetching fetchingError isSaving savingError = thisprops; const input = thisrefs; return <div> isAppSynced && 'app is synced up' isFetching && 'getting todos' fetchingError && 'there was an error getting todos: ' + fetchingError isSaving && 'saving todos' savingError && 'there was an error saving todos: ' + savingError <ul>todos</ul> <input ref="input" type="text" onBlur= /> <button onClick= >Sync</button> <br /> <pre>JSON</pre> </div>; } const ConnectedApp = App; ReactDOM;
As you can see there's a lot of async logic and state we don't want to have to deal with.
This is 62 more LOC than the first version. Here's how you would do it in App 3 with
redux-await
:
App3 demo
App 3
;;;;;;; // this not an API, this is a tributeconst api = { return { ; }; } { return { ; }; } const GET_TODOS = 'GET_TODOS';const ADD_TODO = 'ADD_TODO';const SAVE_APP = 'SAVE_APP';const actions = { return type: GET_TODOS AWAIT_MARKER payload: todos: api ; } { return type: ADD_TODO payload: todo ; } { return { ; } };const initialState = isAppSynced: false todos: ;const todosReducer = { if actiontype === GET_TODOS return ...state isAppSynced: true todos: actionpayloadtodos ; if actiontype === ADD_TODO return ...state isAppSynced: false todos: statetodos ; if actiontype === SAVE_APP return ...state isAppSynced: true ; return state;};const reducer = const store = createStorereducer; { thisprops; } { const dispatch todos isAppSynced statuses errors = thisprops; const input = thisrefs; return <div> isAppSynced && 'app is synced up' statusestodos === 'pending' && 'getting todos' statusestodos === 'failure' && 'there was an error getting todos: ' + errorstodosmessage statusessave === 'pending' && 'saving todos' errorssave && 'there was an error saving todos: ' + errorssavemessage <ul>todos</ul> <input ref="input" type="text" onBlur= /> <button onClick= >Sync</button> <br /> <pre>JSON</pre> </div>; } const ConnectedApp = App; ReactDOM;
This version is very easy to reason about, in fact you can completely ignore the fact that the app is async at all. The todosReducer
didn't need to have a single line changed!
Note that this is 107 LOC compared to app2's 125 LOC
Some pitfalls to watch out for
You must either use this modules connect
or manually spread the await
part of the tree over
mapStateToProps
, you can also choose to name it something other than await
and spread that
yourself too.
redux-await
will name the statuses
and errors
prop the same as the payload prop so try to be
as descriptive as possible when naming payload props since any payload props collision will
overwrite the statuses
/errors
value. For a CRUD app don't always name it something like
records
because when you're loading users.records
the app will also think you're loading
todos.records
How it works:
The middleware checks to see if the AWAIT_MARKER
was set on the action
and if it was then dispatches three events with a [AWAIT_META_CONTAINER]
property on the meta property of the action.
The reducer listens for actions with a meta of [AWAIT_META_CONTAINER]
and
when found will set the await
property of the state accordingly.