π€ A redux library to declaratively interact with any API
Redux Chunk allows you to define your API endpoints across chunks in webpack chunked application. For large APIs, it makes sense to dynamically add paths to your SDK-style endpoints list and it improves the separation of concerns, with each action file defining it's own endpoints and request structure.
There are many API helpers for Redux, we based this library off redux-bees but we created (read: copied) this library to work with APIs that may not be standardized. i.e. it doesn't use JSON API 100%. If your API does, you should consider using redux-bees
.
npm install redux-chunk --save
You can use Redux Chunk in your existing Redux app by following these steps:
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import {
reducer as apiReducer,
middleware as apiMiddleware,
} from 'redux-chunk';
const reducer = combineReducers({
// ...your other reducers
api: apiReducer,
});
const store = createStore(reducer, applyMiddleware(apiMiddleware));
You can define general endpoints in your main bundle or wait to define endpoints in chunked actions. You can configure dynamic headers such as adding the user's access token to Authorization
when they are logged in.
import API { get, post, patch, destroy } from 'redux-chunk';
import { store } from 'index'; // The result of your createStore
const endpoints = {
getItems: { method: get, path: '/items' },
getItem: { method: get, path: '/items/:id' },
createItem: { method: post, path: '/items' },
updateItem: { method: patch, path: '/items/:id' },
deleteItem: { method: destroy, path: '/items/:id' }
};
const options = {
baseUrl: 'https://api.example.com',
configureHeaders: headers => ({
...headers,
Authorization: `Bearer ${store.getState().auth.access_token}`
})
};
const api = new API(endpoints, options);
export default api;
const options = {
baseUrl: (path, placeholders) =>
path.indexOf('other-api') > -1
? 'https:://other.example.com'
: 'https://api.example.com'
};
If you need to execute specific code before or after every request or retry a request if a particular response is returned, you can use the handleResolve
and handleReject
options:
const options = {
baseUrl: 'https://api.example.com',
handleResolve: (req, res) => {
// Only return the result, the request is given to you here for checking sent placeholders or headers
return Promise.resolve({ ...res, extra: 'thing' });
},
handleReject: (req, res) => {
if(res.body.message.indexOf('Access Token Expired') > -1) {
// Refresh the access token then retry the request using .retry() method
return req.retry();
}
return Promise.reject(res);
}
};
api.getItems()
.then(res => {
// {
// status: 200,
// headers: {...},
// body: [
// {
// name: 'my-item',
// price: 100
// }
// ]
// }
})
.catch(err => {
// {
// status: 500,
// headers: {...}
// body: {
// message: 'Something went wrong.'
// }
// }
});
The arguments you pass to your endpoint depend on the HTTP method and the presence of placeholders in the path declared for the endpoint.
api.getItem({ id: 12 });
// GET https://api.example.com/items/12
api.getItem({ id: 12, custom: 'query' });
// GET https://api.example.com/items/12?custom=query
api.createItem({ name: 'my-item', price: 100 });
// POST https://api.example.com/items
api.updateItem({ id: 12 }, { price: 200 });
// PATCH https://api.example/items/12
api.deleteItem({ id: 12 });
// DELETE https://api.example.com/items/12
If you perform multiple concurrent requests to the same endpoint with the same parameters, a single API call will be performed, and every request will be attached to the same promise:
api.getItem({ id: 12 })
.then(data => console.log(data));
// This won't produce a new API call
api.getPost({ id: 12 })
.then(data => console.log(data));
You can dispatch the API request promises as Redux actions which are stored in the state of your application under an api
key.
const getItem = id => {
return api.getItem({ id });
};
With redux-thunk
:
const getItem = id => {
return dispatch => {
// Asynchronous things here
dispatch(api.getItem({ id }))
};
};
It's also possible to mutate the result:
const getItem = id => {
const placeholders = { id };
const req = api.getItem(placeholders);
req.then(res => {
// Do anything to the result
res.body.extras = { extra: 'thing' };
// Return it to the redux-chunk middleware
return res;
});
// Requires these to be added back to the Promise sequence
req.actionName = 'getItem';
req.params = { options: {}, placeholders };
return req;
};
The results of your API requests are cached in the api
section of your Redux state. It should be considered private, and accessed via the query
state selector.
store.getState();
// {
// api: {
// getItem: {
// '{}': {
// isLoading: false,
// error: null,
// headers: {...},
// status: 200,
// result: [...]
// },
// '{"id":12}': {
// isLoading: false,
// error: null,
// headers: {...},
// status: 200,
// result: {...}
// }
// }
// }
// }
The query
function takes the following arguments:
- query(state, apiCall)
- query(state, apiCall, placeholders)
If you don't include placeholders, the query will return all requests that have been made to that endpoint.
Examples:
query(state, api.getItem, { id: 12 })
// {
// hasStarted: true,
// isLoading: false,
// hasFailed: false,
// result: { id: 12, name: 'my-item', price: 100 }
// headers: {...},
// status: 200,
// error: null,
// }
query(state, api.getItems)
// {
// hasStarted: true,
// isLoading: false,
// hasFailed: false,
// result: [
// { payload: { id: 12, name: 'my-item', price: 100 }, params: { id: 12 } }
// ],
// headers: [...],
// status: 200,
// error: null
// }
To make it easier to integrate data fetching in React Components, you can use the query
state selector inside a connect
HOC from the react-redux
lib.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { query } from 'redux-chunk';
import api from 'helpers/api';
const App = ({ item }) => (
<div className="app">
{item.result && !item.isLoading ? (
<p>{item.name}</p>
) : item.hasFailed ? (
<p>{item.error}</p>
) : (
<p>Loading...</p>
)}
</div>
);
export default connect(
state => ({ item: query(state, api.getItems, { id: 12 }) })
)(App);
In action files that are loaded in by chunked modules, you can add new endpoints to your built API with the addEndpoints
function.
import { get } from 'redux-chunk';
import api from 'helpers/api';
api.addEndpoints({
getPosts: { method: get, path: '/posts' }
});
const getPosts = () => {
return api.getPosts();
};