Home

Awesome

redux-catch-promise

Extended replacement of redux-thunk middleware to supporting async-await functions and implement server-side rendering for React components with asynchronous state. See example below.

What’s a thunk?!

A thunk is a function that wraps an expression to delay its evaluation.

// calculation of 1 + 2 is immediate
// x === 3
let x = 1 + 2;

// calculation of 1 + 2 is delayed
// foo can be called later to perform the calculation
// foo is a thunk!
let foo = () => 1 + 2;

Motivation

redux-catch-promise middleware allows you to write action creators that return sync or async functions instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState() as parameters.

An action creator that returns an async functions to perform asynchronous dispatch:

const SHOW_USER_LOCATION = 'SHOW_USER_LOCATION';

function showUserLocation(location) {
  return {
    type: SHOW_USER_LOCATION,
    location
  };
}

function requestUserLocation(userName) {
  return dispatch => async function () {
    const finalURL = 'https://api.github.com/users/' + userName;
    const response = await fetch(URL, {
      method: 'POST'
    });
    const data = await response.json();
    const action = showUserLocation(data['location']);
    dispatch(action);
  };
}

Installation

Upgrade redux-thunk to redux-catch-promise

To enable async actions in a way described above you have to replace redux-thunk to redux-catch-promise. Just do it in 3 steps:

Install the middleware

npm install redux-catch-promise --save

Replace import declaration

import thunk from 'redux-thunk';

should be replaced to:

import CatchPromise from 'redux-catch-promise';

Replace middleware assignment

For example:

const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);

should be replaced to:

const catchPromise = CatchPromise();
const createStoreWithMiddleware = applyMiddleware(catchPromise)(createStore);

Clean installation

Install the middleware

npm install redux-catch-promise --save

Add import declaration

import CatchPromise from 'redux-catch-promise';

Add middleware assignment

const catchPromise = CatchPromise();
const createStoreWithMiddleware = applyMiddleware(catchPromise)(createStore);

How to catch dispatched promises and async functions

import { createStore, applyMiddleware, combineReducers } from 'redux';
import CatchPromise from 'redux-catch-promise';
import * as reducers from './reducers/index';

const reducer = combineReducers(reducers);

const actionPromises = [];
const createStoreWithMiddleware = applyMiddleware(
  CatchPromise((promisedAction, action, store) => {
    // it willbe called only when a Promise dispatched
    actionPromises.push(promisedAction);
  }
)(createStore);

const store = createStoreWithMiddleware(reducer);

Server-side rendering with async state

It's a short demo how to implement with this middleware server-side rendering of your React components with async-loading state:

server.js

const React from 'react';
const Application from './application';
const koa = require('koa');
const app = koa();

app.use(function *(next) {
  const preparePromises = [];
  const serverSideRendering = {
    preparePromises,
    sharedState: {}
  };
  const application = (
    <Application serverSideRendering={serverSideRendering} />
  );
  const prefetchedBody = React.renderToString(application);
  if (preparePromises.length > 0) {
    for (let index = 0, length = preparePromises.length; index < length; index++) {
      yield preparePromises[index];
    }
  }
  // re-render with fetched data if prepare promises are found
  let body;
  if (serverSideRendering.preparePromises.length > 0) {
    body = React.renderToString(
      application
    );
  } else {
    body = prefetchedBody;
  }
  const code = 'window._sharedData = ' + JSON.stringify(sharedData) + ';';
  yield this.render('react-page', {
    'body': body,
    'code': code
  })
});

client.js

import 'isomorphic-fetch';
import React from 'react';
import Application from './application';

const state = (window._sharedData && window._sharedData['state']) || {};
const rootElement = document.getElementById('root');

React.render(<Application state={state} />, rootElement);

application.js

import React, { Component, PropTypes } from 'react';
import { combineReducers, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import CatchPromise from 'redux-catch-promise';
import ReposList from './repos-list';
import reposListReducer from './repos-list/reducer';

export default class Application extends Component {
  static propTypes = {
    state: PropTypes.object,
    serverSideRendering: PropTypes.object
  };

  constructor(props, context) {
    super(props, context);
    const { serverSideRendering } = props;
    const catchPromise = CatchPromise(
      (typeof serverSideRendering === 'object') &&
        (promisedAction, action, store) => {
          serverSideRendering.preparePromises.push(promisedAction);
          serverSideRendering.sharedState = store.getState();
        });
    const createStoreWithMiddleware = applyMiddleware(catchPromise)(createStore);
    const allReducers = combineReducers({
      repostList: repostListReducer
    });
    const store = createStoreWithMiddleware(allReducers, props.state || {});
    if (typeof serverSideRendering === 'object') {
      // callback to dispatch passed actions
      this.prepare = (actions) => actions.forEach((action) => store.dispatch(action));
    }
  }
  
  render() {
    return (
      <Provider store={this.state.store}>
        {this.renderChild.bind(this)}
      </Provider>
    );
  }
  
  renderChild() {
    return (
      <ReposList prepare={this.prepare} />
    );
  }
}

repos-list/index.js

import React, { Component, PropTypes } from 'react';
import { bindActionCreators, connect } from 'react-redux';
import * as projectsListActions from './actions';

function selector(state) {
  return {
    reposList: state.reposList
  };
}

class ReposList extends Component {
  constructor(props, context) {
    super(props, context);
    this.prepareActions = [
      reposListActions.fetch()
    ];
    if (typeof props.prepare === 'function') {
      props.prepare(this.prepareActions);
    }
  }
  
  render() {
    const { reposList } = this.props;
    return (
      <ul>
        {Array.isArray(reposList.items) ?
          reposList.items.map((it) => <li>{it['name']}</li>) :
          <li>Empty</li>}
      </ul>
    );
  }
  
  componentDidMount() {
    if (this.prepareDataActions) {
      const props = this.props;
      this.prepareDataActions.forEach((action) => props.dispatch(action));
    }
  }
}

repos-list/action-types.js

export default {
  UPDATE: 'REPOS_LIST_UPDATE'
};

repos-list/actions.js

import {
  UPDATE
} from './action-types';

export default function fetch (className, force) {
  return async (dispatch, getState) => {
    try {
      const data = await fetch('http://api.github.com/users/DenisIzmaylov/repos');
      const items = await data.json();
    } catch(err) {
      console.error(err);
    }
    dispatch({
      type: UPDATE,
      state: { items }
    });
  }
}

repos-list/reducer.js

import {
  UPDATE
} from './action-types';

const initialState = {
  items: {}
};

export default function (state = initialState, action = {}) {
  switch (action.type) {
    case UPDATE:
      return [action.state, ...state];
    default:
      return state;
  }
}

Thanks

License

MIT