• Stars
    star
    155
  • Rank 240,864 (Top 5 %)
  • Language
    JavaScript
  • Created about 9 years ago
  • Updated almost 9 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Manage async redux actions sanely

redux-await

NPM version Build status Test coverage Downloads

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:

  1. Apply the middleware:

    import { middleware as awaitMiddleware } from 'redux-await';
    let createStoreWithMiddleware = applyMiddleware(
      awaitMiddleware
    )(createStore);
  2. Install the reducer into the await path of your combineReducers

    import reducers from './reducers';
    
    // old code
    // const store = applyMiddleware(thunk)(createStore)(reducers);
    
    // new code
    import { reducer as awaitReducer } from 'redux-await';
    const store = applyMiddleware(thunk, awaitMiddleware)(createStore)({
      ...reducers,
      await: awaitReducer,
    });
  3. Use the connect function from this module and not react-redux's

    // old code
    // import { connect } from 'react-redux';
    
    // new code
    import { connect } from 'redux-await';
    
    class FooPage extends Component {
      render() { /* ... */ }
    }
    
    export default connect(state => state.foo)(FooPage)

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
import { AWAIT_MARKER } from 'redux-await';
export const getTodos = () => ({
  type: GET_TODOS,
  AWAIT_MARKER,
  payload: {
    loadedTodos: api.getTodos(), // returns promise
  },
});
export const addTodo = todo => ({
  type: ADD_TODO,
  AWAIT_MARKER,
  payload: {
    savedTodo: api.saveTodo(todo), // returns promise
  },
});

Now your containers barely need to change:

class Container extends Component {
  render() {
    const { todos, statuses, errors } = this.props;

    // old code
    //return <div>
    //  <MyList data={todos} />
    //</div>;

    // new code
    return <div>
      { statuses.loadedTodos === 'pending' && <div>Loading...</div> }
      { statuses.loadedTodos === 'success' && <MyList data={loadedTodos} /> }
      { statuses.loadedTodos.status === 'failure' && <div>Oops: {errors.loadedTodos.message}</div> }
      { statuses.savedTodo === 'pending' && <div>Saving new savedTodo</div> }
      { statuses.savedTodo === 'failure' && <div>There was an error saving</div> }
    </div>;
  }
}

//old code
// import { connect } from 'react-redux';

// new code
import { connect } from 'redux-await'; // it just spreads state.await on props

export default connect(state => state.todos)(Container)

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

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider, connect } from 'react-redux';
import { applyMiddleware, createStore, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';

const GET_TODOS = 'GET_TODOS';
const ADD_TODO = 'ADD_TODO';
const SAVE_APP = 'SAVE_APP';
const actions = {
  getTodos() {
    const todos = JSON.parse(localStorage.todos || '[]');
    return { type: GET_TODOS, payload: { todos } };
  },
  addTodo(todo) {
    return { type: ADD_TODO, payload: { todo } };
  },
  saveApp() {
    return (dispatch, getState) => {
      localStorage.todos = JSON.stringify(getState().todos.todos);
      dispatch({ type: SAVE_APP });
    }
  },
};
const initialState = { isAppSynced: false, todos: [] };
const todosReducer = (state = initialState, action = {}) => {
  if (action.type === GET_TODOS) {
    return { ...state, isAppSynced: true, todos: action.payload.todos };
  }
  if (action.type === ADD_TODO) {
    return { ...state, isAppSynced: false, todos: state.todos.concat(action.payload.todo) };
  }
  if (action.type === SAVE_APP) {
    return { ...state, isAppSynced: true };
  }
  return state;
};
const reducer = combineReducers({
  todos: todosReducer,
})
const store = applyMiddleware(thunk, createLogger())(createStore)(reducer);

class App extends Component {
  componentDidMount() {
    this.props.dispatch(actions.getTodos());
  }
  render() {
    const { dispatch, todos, isAppSynced } = this.props;
    const { input } = this.refs;
    return <div>
      {isAppSynced && 'app is synced up'}
      <ul>{todos.map(todo => <li>{todo}</li>)}</ul>
      <input ref="input" type="text" onBlur={() => dispatch(actions.addTodo(input.value))} />
      <button onClick={() => dispatch(actions.saveApp())}>Sync</button>
      <br />
      <pre>{JSON.stringify(store.getState(), null, 2)}</pre>
    </div>;
  }
}
const ConnectedApp = connect(state => state.todos)(App);

ReactDOM.render(<Provider store={store}><ConnectedApp /></Provider>, document.getElementById('root'));

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

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider, connect } from 'react-redux';
import { applyMiddleware, createStore, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';

// this not an API, this is a tribute
const api = {
  save(data) {
    return new Promise(resolve => {
      setTimeout(() => {
        localStorage.todos = JSON.stringify(data);
        resolve(true);
      }, 2000);
    });
  },
  get() {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(JSON.parse(localStorage.todos || '[]'));
      }, 1000);
    });
  }
}

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 = {
  getTodos() {
    return dispatch => {
      dispatch({ type: GET_TODOS_PENDING });
      api.get()
        .then(todos => dispatch({ type: GET_TODOS, payload: { todos } }))
        .catch(error => dispatch({ type: GET_TODOS_ERROR, payload: error, error: true }))
      ;
      ;
    }
  },
  addTodo(todo) {
    return { type: ADD_TODO, payload: { todo } };
  },
  saveApp() {
    return (dispatch, getState) => {
      dispatch({ type: SAVE_APP_PENDING });
      api.save(getState().todos.todos)
        .then(() => dispatch({ type: SAVE_APP }))
        .catch(error => dispatch({ type: SAVE_APP_ERROR, payload: error, error: true }))
      ;
    }
  },
};
const initialState = {
  isAppSynced: false,
  isFetching: false,
  fetchingError: null,
  isSaving: false,
  savingError: null,
  todos: [],
};
const todosReducer = (state = initialState, action = {}) => {
  if (action.type === GET_TODOS_PENDING) {
    return { ...state, isFetching: true, fetchingError: null };
  }
  if (action.type === GET_TODOS) {
    return {
      ...state,
      isAppSynced: true,
      isFetching: false,
      fetchingError: null,
      todos: action.payload.todos,
    };
  }
  if (action.type === GET_TODOS_ERROR) {
    return { ...state, isFetching: false, fetchingError: action.payload.message };
  }
  if (action.type === ADD_TODO) {
    return { ...state, isAppSynced: false, todos: state.todos.concat(action.payload.todo) };
  }
  if (action.type === SAVE_APP_PENDING) {
    return { ...state, isSaving: true, savingError: null };
  }
  if (action.type === SAVE_APP) {
    return { ...state, isAppSynced: true, isSaving: false, savingError: null };
  }
  if (action === SAVE_APP_ERROR) {
    return { ...state, isSaving: false, savingError: action.payload.message }
  }
  return state;
};
const reducer = combineReducers({
  todos: todosReducer,
})
const store = applyMiddleware(thunk, createLogger())(createStore)(reducer);

class App extends Component {
  componentDidMount() {
    this.props.dispatch(actions.getTodos());
  }
  render() {
    const { dispatch, todos, isAppSynced, isFetching, fetchingError, isSaving, savingError } = this.props;
    const { input } = this.refs;
    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.map(todo => <li>{todo}</li>)}</ul>
      <input ref="input" type="text" onBlur={() => dispatch(actions.addTodo(input.value))} />
      <button onClick={() => dispatch(actions.saveApp())}>Sync</button>
      <br />
      <pre>{JSON.stringify(store.getState(), null, 2)}</pre>
    </div>;
  }
}

const ConnectedApp = connect(state => state.todos)(App);

ReactDOM.render(<Provider store={store}><ConnectedApp /></Provider>, document.getElementById('root'));

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

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { applyMiddleware, createStore, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import createLogger from 'redux-logger';
import {
  AWAIT_MARKER,
  createReducer,
  connect,
  reducer as awaitReducer,
  middleware as awaitMiddleware,
} from 'redux-await';

// this not an API, this is a tribute
const api = {
  save(data) {
    return new Promise(resolve => {
      setTimeout(() => {
        localStorage.todos = JSON.stringify(data);
        resolve(true);
      }, 2000);
    });
  },
  get() {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(JSON.parse(localStorage.todos || '[]'));
      }, 1000);
    });
  }
}

const GET_TODOS = 'GET_TODOS';
const ADD_TODO = 'ADD_TODO';
const SAVE_APP = 'SAVE_APP';
const actions = {
  getTodos() {
    return {
      type: GET_TODOS,
      AWAIT_MARKER,
      payload: {
        todos: api.get(),
      },
    };
  },
  addTodo(todo) {
    return { type: ADD_TODO, payload: { todo } };
  },
  saveApp() {
    return (dispatch, getState) => {
      dispatch({
        type: SAVE_APP,
        AWAIT_MARKER,
        payload: {
          save: api.save(getState().todos.todos),
        },
      });
    }
  },
};
const initialState = { isAppSynced: false, todos: [] };
const todosReducer = (state = initialState, action = {}) => {
  if (action.type === GET_TODOS) {
    return { ...state, isAppSynced: true, todos: action.payload.todos };
  }
  if (action.type === ADD_TODO) {
    return { ...state, isAppSynced: false, todos: state.todos.concat(action.payload.todo) };
  }
  if (action.type === SAVE_APP) {
    return { ...state, isAppSynced: true };
  }
  return state;
};
const reducer = combineReducers({
  todos: todosReducer,
  await: awaitReducer,
})

const store = applyMiddleware(thunk, awaitMiddleware, createLogger())(createStore)(reducer);

class App extends Component {
  componentDidMount() {
    this.props.dispatch(actions.getTodos());
  }
  render() {
    const { dispatch, todos, isAppSynced, statuses, errors } = this.props;
    const { input } = this.refs;
    return <div>
      {isAppSynced && 'app is synced up'}
      {statuses.todos === 'pending' && 'getting todos'}
      {statuses.todos === 'failure' && 'there was an error getting todos: ' + errors.todos.message}
      {statuses.save === 'pending' && 'saving todos'}
      {errors.save && 'there was an error saving todos: ' + errors.save.message}
      <ul>{todos.map(todo => <li>{todo}</li>)}</ul>
      <input ref="input" type="text" onBlur={() => dispatch(actions.addTodo(input.value))} />
      <button onClick={() => dispatch(actions.saveApp())}>Sync</button>
      <br />
      <pre>{JSON.stringify(store.getState(), null, 2)}</pre>
    </div>;
  }
}


const ConnectedApp = connect(state => state.todos)(App);

ReactDOM.render(<Provider store={store}><ConnectedApp /></Provider>, document.getElementById('root'));

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.

More Repositories

1

immutability-helper

mutate a copy of data without changing the original source
TypeScript
5,174
star
2

exercises

Some basic javascript coding challenges and interview questions
JavaScript
4,174
star
3

safetest

TypeScript
1,307
star
4

wavy

use ~ in require and import calls
JavaScript
379
star
5

nip

Node Input/output Piper
JavaScript
291
star
6

webwork

Execute Web Workers without external files
JavaScript
229
star
7

zerobox

JavaScript
148
star
8

jsan

handle circular references when stringifying and parsing
JavaScript
90
star
9

redux-create-reducer

Publishing createReducer from http://redux.js.org/docs/recipes/ReducingBoilerplate.html#generating-reducers
JavaScript
84
star
10

weak-key

Get a unique key for an object ( mainly for react's key={} )
JavaScript
76
star
11

ts-cookbook

A collection of delicious Typescript recipes
TypeScript
44
star
12

screenliner

node util for writing to regions on the terminal
JavaScript
41
star
13

deact

react inspired DOM element template engine
JavaScript
33
star
14

TinyRouter.php

Simple Router For PHP
PHP
26
star
15

w8

Give promises and thunks timeouts
JavaScript
25
star
16

run-every

run a command over and over
JavaScript
23
star
17

crlf

get and set line endings - Pull Requests welcome
JavaScript
23
star
18

react-mocha-starter

Scaffolding for a react project with mocha testing in node and phantom
JavaScript
18
star
19

nester

Get and set properties from deeply nested arrays
JavaScript
16
star
20

zerovalidate

A minimalist javascript form validator
JavaScript
12
star
21

redux-browserify

it can be done
JavaScript
12
star
22

redux-standard-action

A human-friendly standard for Redux action objects.
JavaScript
11
star
23

jq2

extract json data
JavaScript
10
star
24

wttt

When This Then That
JavaScript
10
star
25

zan

test object types (similar to React.PropTypes)
JavaScript
7
star
26

httpies

httpie with extra headers
JavaScript
7
star
27

poel

Create a pool of cluster workers.
JavaScript
6
star
28

react-compose-wrappers

TypeScript
6
star
29

typeaheadObj

Twitter Bootstrap Typeahead With Objects
JavaScript
6
star
30

git-tree-maker

JavaScript
6
star
31

is_googlebot.php

PHP
4
star
32

zerocomplete

JavaScript
4
star
33

tweet-editor

HTML
3
star
34

mithril-todo-msx

JavaScript
3
star
35

fizzbuzz

JavaScript
3
star
36

reglob

require a glob
JavaScript
3
star
37

will-they-sue

list the license of all your deps
JavaScript
3
star
38

ccolors

Cli Colors, a slightly improved version of npm's colors
JavaScript
3
star
39

member-berry

Memoize a function of n args with O(n) recall and no memory leaks.
JavaScript
3
star
40

Db.class.php

PHP
2
star
41

react-track-events

TypeScript
2
star
42

babel-plugin-hodor

JavaScript
2
star
43

zerolay

JavaScript
2
star
44

jquery.save

jQuery.save
2
star
45

xurl

A simple url parser
JavaScript
2
star
46

es6-module-boilerplate

boilerplate for es6 -> es5 module
JavaScript
2
star
47

logen

lodash like library for generators
JavaScript
1
star
48

talks

HTML
1
star
49

Timer.class.php

PHP
1
star
50

CacheSQL

CacheSQL class
PHP
1
star
51

co-phantom

JavaScript
1
star
52

tuplizer

Powerful typescript types to manipulate tuples
TypeScript
1
star
53

wrt

A simple react router
JavaScript
1
star
54

torah

Python
1
star
55

webcam-switcher

TypeScript
1
star
56

promise-utils

Useful utils to use native promises like bluebird
JavaScript
1
star
57

isIE10

javascript test if browser is IE10
JavaScript
1
star
58

reflux-create-reducer

Publishing createReducer from http://rackt.github.io/redux/docs/recipes/ReducingBoilerplate.html
JavaScript
1
star
59

m2

Lightweight Mocha Clone
JavaScript
1
star
60

node-webkit-stater

JavaScript
1
star
61

rxjs-props

Like Promise.props but for rxjs
JavaScript
1
star
62

octopress-test-case

Shell
1
star
63

toopl

Typescript tuple
JavaScript
1
star
64

github-tab-resizer

JavaScript
1
star
65

react-override

TypeScript
1
star
66

require-es6

Require es6 version of module if available as `main-es6` prop in package.json
JavaScript
1
star