React Redux Fetch
A declarative and customizable way to fetch data for React components and manage that data in the Redux state.
Table of contents
- Goal
- Motivation
- Installation
- Setup
- Basic example
- How does it work?
- API
- Examples
- Code snippets
- Versioning
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
Setup
-
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
-
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;
Basic example
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
};
componentWillMount() {
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);
How does it work?
Every entry in the config array passed to connect()
is mapped to 2 properties, a function to make the actual request and an object containing the response.
The function name consists of 3 parts:
- dispatch: to indicate that by calling this function a redux action is dispatched
- [resourceName]: the name of the resource declared in the config
- [method]: The method of the request (Get/Delete/Post/Put)
The response object, with name: [resourceName] + 'Fetch', consists of:
- pending, fulfilled, rejected: Promise flags
- value: The actual response body
- meta: The actual response object
When calling this.props.dispatchAllPokemonGet();
, react-redux-fetch dispatches the action react-redux-fetch/GET_REQUEST
:
The action creates a new state tree allPokemon
, inside the repository
state tree:
The react-redux-fetch middleware takes this action and builds the request with Fetch API. This part of the state is passed as a prop to the PokemonList component:
When the request fulfills (i.e. receiving a status code between 200 and 300), react-redux-fetch dispatches the action react-redux-fetch/GET_FULFIL
:
With updated state tree:
This part of the state is passed as a prop to the PokemonList component:
API
connect()
A higher order component to enhance your component with the react-redux-fetch functionality.
Accepts an array:
connect([{
// ... configuration, see below
}])(yourComponent);
Or a function returning an array. This function receives the props and context, which can then be used in your configuration to dynamically build your urls.
connect((props, context) => [{
// ... configuration, see below
}])(yourComponent);
The returned array should be an array of objects, with the following properties:
resource
: Object|String, required. When used as a string, this is the same asresource: { name: 'myResource' }
.name
: String, required. A name for your resource, this name will be used as a key in the state tree. If noaction
is defined inresource
, thename
is used in the dispatch prop, e.g.:name: 'myResource'
=>dispatchMyResourceGet
.action
: String, optional. A name to use in the dispatch function that's created and passed as a prop. (e.g.action: 'myAction'
=>dispatchMyActionGet
).
method
: String, optional, default: 'get'. The request method that will be used for the request. One of 'get', 'post', 'put', 'delete'. Can be extended by adding new types to the registry (see below).request
: Object|Function, required. Use a function if you want to pass dynamic data to the request config (e.g. body data).url
: String, required. The URL to make the request to.body
: Object, optional. The object that will be sent as JSON in the body of the request.headers
: Object|Function, optional. Use this to set the headers, for this request only. Usecontainer.registerRequestHeader()
to set headers for every request.meta
: Object, optional. Everything passed to 'meta' will be passed to every part in the react-redux-fetch flow.comparison
: Any, optional. If provided, a new request is not made if thecomparison
value between dispatch calls is the same.force
: boolean, optional. Iftrue
, overrules thecomparison
property.
container
import { container } from 'react-redux-fetch';
The container provides a single entry point into customizing the different parts of react-redux-fetch. For now, the following customizations are possible, this will be extended in the future:
-
requestMethods
Out-of-the-box, react-redux-refetch provides implementations for
get
,post
,put
anddelete
requests. A new request method, e.g.patch
, can be added like this:container.registerRequestMethod('patch', { method: 'patch', // The request method middleware: fetchRequest, // The middleware to handle the actual fetching. 'fetchRequest' from 'react-redux-fetch' is a sensible default for any request method. reducer: patchReducer });
An existing request method definition can be altered like this:
// Replace middleware for POST requests with a mock container.changeRequestMethodConfig('post', 'middleware', mockFetchMiddleware);
-
requestHeaders
The default request headers are
'Accept': 'application/json'
and'Content-Type': 'application/json'
. You can add request headers:container.registerRequestHeader('authorization', 'Bearer some.jwt.token');
Or replace the request headers:
container.replaceRequestHeaders({ 'Content-Type', 'application/xml' });
-
reducers
Additional reducers can be registered to work on a subset of the fetch state, without having to overwrite all reducers defined in requestMethods definition. For example, there is no out-of-the-box way of clearing state data. If you want to clear e.g. all todo items from a todo list, you can register a reducer to work on the 'todos' state.
container.registerReducer('todos', todosReducer);
The todos state slice is passed to the reducer, which can return a new state when your custom redux action is dispatched:
function todosReducer(state, action) { switch (action.type) { case 'TODOS_RESET': return state.set('value', null); } return state; }
-
requestBuilder
The requestBuilder is used by the default react-redux-fetch middleware. Takes a URL and request config and returns a Request object. To replace the default implementation:
container.getDefinition('requestBuilder').replaceArgument('build', customRequestBuilder);
buildActionsFromMappings
import { buildActionsFromMappings } from 'react-redux-fetch';
The function internally used by connect()
. You can use this function to create the fetch redux actions without a React Component.
buildActionsFromMappings(config)
accepts the same configuration options as connect()
.
const actions = buildActionsFromMappings([{
resource: 'todos',
request: {
url: apiRoutes.getTodos(),
},
}]);
store.dispatch(actions.todosGet());
Examples
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 deleteid
: The id-value of the object to find and delete
Code snippets
Versioning
Semver is followed as closely as possible. For updates and migration instructions, see the changelog.