Skip to content

Instantly share code, notes, and snippets.

@thomasboyt
Last active November 30, 2016 19:55
Show Gist options
  • Select an option

  • Save thomasboyt/ca8edefb0ef07b8ea9077f50d5464575 to your computer and use it in GitHub Desktop.

Select an option

Save thomasboyt/ca8edefb0ef07b8ea9077f50d5464575 to your computer and use it in GitHub Desktop.

redux "request" idea

(this is adapted from the redux-happy-async project I made earlier this year)

Usage

Using Request State

A "request" is simply a tiny little state machine that lives in a request reducer. It's keyed off of a name (usually an action type) and, optionally, a unique key to help track multiple requests of the same type.

By keeping a request in your Redux state, you can easily trigger a request (through an action creator) in a component and react to the loading/error states of that request in a different component. It is also easy to prevent multiple, identical requests from being in-flight at once.

The request holds the following state:

  • status: This is either REQUEST_IDLE, REQUEST_PENDING REQUEST_SUCCESS, or REQUEST_ERROR.
  • error: This is an error value supplied by the action creator when the error state is entered.
  • key: This is the unique key of the request, if supplied.

Within a component, you can fetch a request from the state:

import {getRequest} from 'LIB_NAME';

function mapStateToProps(state, props) {
  return {
    // gets the request for actionTypes.getUser with the unique key props.userId
    request: getRequest(state, actionTypes.getUser, props.userId);
  };
}

Creating Requests

Requests are created in action creators:

import {requestStart, requestError, requestSuccess} from 'LIB_NAME';

export function fetchUser(userId) {
  return async (dispatch) => {
    dispatch(requestStart(actionTypes.fetchUser, userId));

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

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

      dispatch(requestError(actionTypes.fetchUser, userId, error));
      return;
    }

    dispatch(requestSuccess(actionTypes.fetchUser, userId));

    const data = await resp.json();

    dispatch({
      type: actionTypes.fetchUser,
      user: data,
    });
  };
}

If you attempt to start an already-started request, an error will be thrown.

Resetting Requests

To reset a request - for example, to ensure that when you exit and return to a page, you do not see state from a past request - simply use:

import {requestReset} from 'LIB_NAME';
dispatch(requestReset(actionTypes.getUser, userId));

TODO: can an in-flight request be reset?

Full Example

// components/UserDisplay.jsx

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

import {getRequest, REQUEST_ERROR, REQUEST_SUCCESS} from 'LIB_NAME';

import fetchUser from '../actions/fetchUser';
import * as actionTypes from '../actionTypes';

class DisplayUser extends React.Component {
  componentWillMount() {
    const {id, dispatch} = this.props;
    dispatch(fetchUser(id));
  }

  render() {
    const {request, user} = this.props;

    if (request.status === REQUEST_SUCCESS) {
      return (
        <span>Username: {user}</span>;
      );
    } else if (request.status === REQUEST_ERROR) {
      return (
        <span>Error fetching user: {request.error}</span>
      );
    }

    return <span>Loading user...</span>;
  }
}

function mapStateToProps(state, props) {
  const {id} = props;

  return {
    request: getRequest(state, actionTypes.fetchUser, id);
    user: state.users[id],
  };
}

export default connect(mapStateToProps)(UserDisplay);
// actions/fetchUser.js

import {requestStart, requestError, requestSuccess} from 'LIB_NAME';

export function fetchUser(userId) {
  return async (dispatch) => {
    dispatch(requestStart(actionTypes.fetchUser, userId));

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

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

      dispatch(requestError(actionTypes.fetchUser, userId, error));
      return;
    }

    dispatch(requestSuccess(actionTypes.fetchUser, userId));

    const data = await resp.json();

    dispatch({
      type: actionTypes.fetchUser,
      user: data,
    });
  };
}
// reducers/users.js

export default function userReducer(state = {}, action) {
  if (action.type === actionTypes.fetchUser) {
    const {id} = action.user;

    return {
      [id]: action.user,
      ...state,
    };
  }

  return state;
}

FAQ

Can my own reducers handle the request* action creators?

They potentially could, but a better way to handle custom state updating would be to simply dispatch another action if an error is encountered.

How can I have multiple instances of a request if there is no obvious unique key to use?

You can create a counter in your action creator:

let todoRequestId = 0;
export function createTodo(text) {
  return async (dispatch) => {
    dispatch(requestStart(actionTypes.createTodo, todoRequestId));
    // ...
  };
}

You'll likely want to then get a list of pending requests to be able to reference/display these requests (see below).

How can I manage pending requests of a given type?

You can filter for requests from the async reducer:

const createTodoRequests = store.getState().requests[actionTypes.createTodo];
const pending = createTodoRequests.map((request) => request.status === REQUEST_PENDING);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment