react redux fetch

A declarative and customizable way to fetch data for React components and manage that data in the Redux state

70
17
JavaScript

React Redux Fetch

A declarative and customizable way to fetch data for React components and manage that data in the Redux state.

build status npm version

Documentation

Goal

The goal of this library is to minimize boilerplate code of crud operations in react/redux applications.

Motivation

Redux provides a clean interface for handling data across your application, but integrating with a web service can become a quite cumbersome, repetitive task. React-refetch by Heroku provides a good alternative, but doesn’t keep your fetched data in the application state, which makes it more difficult to debug, handle side effects (e.g. with redux-saga) and integrate with your redux actions. This module is strongly inspired by react-refetch; it exposes a connect() decorator to keep your components stateless. This function lets you map props to URLs. React-redux-fetch takes these mappings and creates functions which dispatch actions and passes them as props to your component. The response is also passed as a prop to your component with additional pending, fulfilled and rejected flags, just like react-refetch.

Installation

npm install --save react-redux-fetch

yarn add react-redux-fetch

Setup

  1. Connect the react-redux-fetch middleware to the Store using applyMiddleware:

    // ...
    import { createStore, applyMiddleware } from 'redux';
    import { middleware as fetchMiddleware } from 'react-redux-fetch';
    
    // ...
    
    const store = createStore(reducer, applyMiddleware(fetchMiddleware));
    
    // rest unchanged
    
  2. Mount react-redux-fetch reducer to the state at repository:

    import { combineReducers } from 'redux';
    import { reducer as fetchReducer } from 'react-redux-fetch';
    
    const rootReducer = combineReducers({
      // ... other reducers
      repository: fetchReducer,
    });
    
    export default rootReducer;
    

Examples

react-redux-fetch exposes two ways to connect your component to your api configuration:

  1. Render prop
  2. Higher order Component

GET

Render props

import React from 'react';
import { ReduxFetch } from 'react-redux-fetch';

const fetchConfig = [
  {
    resource: 'allPokemon',
    method: 'get', // You can omit this, this is the default
    request: {
      url: 'http://pokeapi.co/api/v2/pokemon/',
    },
  },
];

const PokemonList = () => (
  <ReduxFetch config={fetchConfig} fetchOnMount>
    {({ allPokemonFetch }) => {
      if (allPokemonFetch.rejected) {
        return <div>Oops... Could not fetch Pokémon!</div>;
      }

      if (allPokemonFetch.fulfilled) {
        return (
          <ul>
            {allPokemonFetch.value.results.map(pokemon => (
              <li key={pokemon.name}>{pokemon.name}</li>
            ))}
          </ul>
        );
      }

      return <div>Loading...</div>;
    }}
  </ReduxFetch>
);

export default PokemonList;

Higher order component

import React, { PropTypes } from 'react';
import connect from 'react-redux-fetch';

class PokemonList extends React.Component {
  static propTypes = {
    // injected by react-redux-fetch
    /**
     * @var {Function} dispatchAllPokemonGet call this function to start fetching all Pokémon
     */
    dispatchAllPokemonGet: PropTypes.func.isRequired,
    /**
     * @var {Object} allPokemonFetch contains the result of the request + promise state (pending, fulfilled, rejected)
     */
    allPokemonFetch: PropTypes.object,
  };

  componentDidMount() {
    this.props.dispatchAllPokemonGet();
  }

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

    if (allPokemonFetch.rejected) {
      return <div>Oops... Could not fetch Pokémon!</div>;
    }

    if (allPokemonFetch.fulfilled) {
      return (
        <ul>
          {allPokemonFetch.value.results.map(pokemon => (
            <li key={pokemon.name}>{pokemon.name}</li>
          ))}
        </ul>
      );
    }

    return <div>Loading...</div>;
  }
}

// connect(): Declarative way to define the resource needed for this component
export default connect([
  {
    resource: 'allPokemon',
    method: 'get', // You can omit this, this is the default
    request: {
      url: 'http://pokeapi.co/api/v2/pokemon/',
    },
  },
])(PokemonList);

POST

import React, {PropTypes} from 'react';
import connect from 'react-redux-fetch';

class Playground extends React.Component {
    static propTypes = {
        // injected by parent
        pokemonOnField: PropTypes.object.isRequired,
        // injected by react-redux-fetch
        dispatchPokemonPost: PropTypes.func.isRequired,
        pokemonFetch: PropTypes.object
    };

    handleCatchPokemon = () => {
        const {pokemonOnField, dispatchPokemonPost} = this.props;
        dispatchPokemonPost(pokemonOnField.id, pokemonOnField.name, pokemonOnField.sprites.front_default);
    };

    render() {
        const {pokemonOnField, pokemonFetch} = this.props;

        return (
            <div>
                <h3>{pokemonOnField.name}</h3>
                <img alt={pokemonOnField.name} src={pokemonOnField.sprites.front_default}/>
                {!pokemonFetch &&
                <button onClick={this.handleCatchPokemon}>catch!</button>
                }
            </div>
        );
    }
}


export default connect([{
    resource: 'pokemon',
    method: 'post',
    request: (id, name, image) => ({
        url: '/api/pokemon/catch',
        body: {
            id,
            name,
            image
        })
}])(Playground);

PUT

Analogous to POST

DELETE

import React, {PropTypes} from 'react';
import connect from 'react-redux-fetch';

class Pokemon extends React.Component {
    static propTypes = {
        // injected by parent
        myPokemon: PropTypes.object.isRequired,
        // injected by react-redux-fetch
        dispatchPokemonDelete: PropTypes.func.isRequired
    };

    handleReleasePokemon = () => {
        this.props.dispatchPokemonDelete(this.props.myPokemon.id);
    };

    render() {
        const {myPokemon, dispatchPokemonDelete} = this.props;

        return (
            <div>
                <h3>{myPokemon.name}</h3>
                <img alt={myPokemon.name} src={myPokemon.image}/>
                <button onClick={this.handleReleasePokemon}>catch!</button>
            </div>
        );
    }
}


export default connect([{
    resource: 'pokemon',
    method: 'delete',
    request: (id) => ({
        url: `/api/pokemon/${id}/release`,
        meta: {
            removeFromList: {
                idName: 'id',
                id: id
            }
        }
}])(Pokemon);

A special property removeFromList can be specified in meta, which removes an element from the state if the resource value is a list.
(In the example, the pokemon state contains a collection of Pokémon.)

  • idName: The id-key of the object to find and delete
  • id: The id-value of the object to find and delete

Code snippets

Code snippets

Versioning

Semver is followed as closely as possible. For updates and migration instructions, see the changelog.