npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

redux-happy-async

v0.0.4

Published

async state with less boilerplate

Downloads

4

Readme

redux-happy-async

(this library isn't battle-tested or recommended for usage yet!)

Tries to cut some of the more obnoxious boilerplate out of handling async state in Redux.

Basically, this adds an async reducer to your store's state that contains async state per action.

Why?

Managing async action state in Redux is a very, very common topic of discussion. The classical way to do it is to create three action types (for start, error, and success), and then create boilerplate to set loading/error states for each action inside a reducer. This boilerplate adds up fast if you're creating an app with lots of async actions!

redux-happy-async abstracts over this pattern and keeps this boilerplate out of your reducers. To do this, it adds an async reducer to that tracks action states which your components can read from.

Example

https://github.com/thomasboyt/earthling-github-issues/tree/async-rewrite

Usage

First, add the async reducer and middleware to your Redux store:

import {createStore, combineReducers, applyMiddleware} from 'redux';
import {asyncReducer, asyncMiddleware} from 'redux-happy-async';

// thunk middleware is optional, but used in below examples
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, asyncMiddleware)(createStore);

const store = createStoreWithMiddleware(combineReducers({
  async: asyncReducer,
  // ...
}));

Then, write your reducer like normal. Note that any async actions you have will only reach your reducer if the ACTION_SUCCESS status is part of the payload. In the below example, the reducer is only called with {type: LOAD_TODOS} once the todos have successfully been loaded:

const State = I.Record({
  todos: null,
});

export default function todoReducer(state=new State(), action) {
  switch (action.type) {
    case LOAD_TODOS:
      // This action is only actually received by the reducer if `asyncStatus: ACTION_SUCCESS` is part
      // of the payload!
      return state.set('todos', action.todos);
    default:
      return state;
  }
}

Then, create an action creator that uses the asyncStatus fields in its payloads.

import {ACTION_START, ACTION_SUCCESS, ACTION_ERROR} from 'redux-happy-async';

export function getTodos() {
  return async function(dispatch) {
    dispatch({
      type: LOAD_TODOS,
      // This special `asyncStatus` field is read by the async middleware
      // to update the async reducer
      asyncStatus: ACTION_START
    });

    const resp = await window.fetch(/*...*/);

    if (resp.status !== 200) {
      const err = await resp.json();

      dispatch({
        type: LOAD_TODOS,
        asyncStatus: ACTION_ERROR,
        // This field is set on the action's async state as `error`
        error: err,
      });
    }

    const data = await resp.json();

    dispatch({
      type: LOAD_TODOS,
      asyncStatus: ACTION_SUCCESS,
      todos: data
    });
  };
}

Then inside your component you could do something like:

import React from 'react';
import {connect} from 'react-redux';

import {getAsyncState} from 'redux-happy-async';

const TodosList = React.createClass({
  componentWillMount() {
    this.props.dispatch(getTodos());
  },

  render() {
    if (this.props.loadingState.loading || !this.props.todos) {
      return <span>Loading...</span>;

    } else if (this.props.loadingState.error) {
      return <span>Encountered error loading</span>;

    } else {
      return todos.map((todo) => {/*...*/});
    }
  }
});

function select(state) {
  return {
    todos: state.todos.todos,
    // returns an object of shape {loading: true/false, error: obj/null}
    loadingState: getAsyncState(state, LOAD_TODOS)
  };
}

export default connect(select)(TodosList);

You can also create further abstractions, look in example/ for some.

Using Unique IDs

Of course, in some cases, your application may have multiple inflight actions of the same type. For example, imagine a todo list with a "complete" action that saves to a very, very slow server. You might click the "complete" checkbox for multiple items at once, and need to separately track the state of each item's "complete" action.

In that case, you'll want to set a uniqueId on the action payload that the async reducer will use to determine which state to update. For example, given a "complete todo" action:

import {ACTION_START, ACTION_SUCCESS, ACTION_ERROR} from 'redux-happy-async';
import {COMPLETE_TODO} from '../ActionTypes';

export function completeTodo(todoId) {
  return async function(dispatch) {
    dispatch({
      type: COMPLETE_TODO,
      asyncStatus: ACTION_START,

      // we pass todoId here since it is the "unique key" for this action
      uniqueId: todoId,
    });

    const resp = await window.fetch(/*...*/);

    if (resp.status !== 200) {
      const err = await resp.json();

      dispatch({
        type: COMPLETE_TODO,
        asyncStatus: ACTION_ERROR,
        uniqueId: todoId,
        error: err,
      });
    }

    dispatch({
      type: COMPLETE_TODO,
      asyncStatus: ACTION_SUCCESS,
      uniqueId: todoId,
    });
  };
}

And this reducer:

const State = I.Record({
  todos: null,
});

export default function todoReducer(state=new State(), action) {
  switch (action.type) {
    case LOAD_TODOS:
      return state.set('todos', I.Map(action.todos.map((todo) => [todo.id, I.Map(todo)])));
    case COMPLETE_TODO:
      return state.setIn(['todos', action.id, 'complete'], true);
    default:
      return state;
  }
}

You could create an individual todo component that displays the async state of its complete action:

import React from 'react';
import {connect} from 'react-redux';

import {getAsyncState} from 'redux-happy-async';

import {COMPLETE_TODO} from '../ActionTypes';
import {completeTodo} from './actions/TodoActions';

const Todo = React.createClass({
  propTypes: {
    todoId: React.PropTypes.number.isRequired,
  },

  handleComplete() {
    this.props.dispatch(completeTodo(this.props.todoId));
  },

  renderComplete() {
    const {completeAsyncState} = this.props;

    if (completeAsyncState.error) {
      return (
        <span>error completing. <a onClick={this.handleComplete}>retry?</a></span>
      );
    } else if (completeAsyncState.loading) {
      return (
        <span>loading...</span>
      );
    } else {
      return (
        <a onClick={this.handleComplete}>complete</a>
      );
    }
  },

  render() {
    const {todo} = this.props;

    return (
      <li>
        {todo.text}
        {' '}
        {todo.completed === false &&
          <a onClick={this.handleComplete}>complete</a>}
      </li>
    );
  }
});

function select(state, props) {
  const {todoId} = props;

  return {
    todo: state.todos.todos.get(todoId),

    // Note the third argument to getAsyncState!
    completeAsyncState: getAsyncState(state, COMPLETE_TODO, todoId)
  };
}

export default connect(select)(TodosList);

API

Action payload fields

  • asyncStatus: one of ACTION_START, ACTION_SUCCESS, or ACTION_ERROR. Setting this field is what tells the async middleware to handle this action as an async action.
  • error: an error that will be set on the async state object (see below). This can be whatever you want as long as your component knows how to consume it (e.g. an error response from your API, a string respresentation of an error...).
  • uniqueId: the unique ID used to track multiple inflight actions of the same type.

getAsyncState(state, actionType, [id])

Returns an object of form {loading, error} representing the current state.

resetAction(type, {all, uniqueId})

Action creator that will reset action state for a given action type.