Giter VIP home page Giter VIP logo

redux-bees's Introduction

redux-bees

A nice, declarative way of managing JSON API calls with Redux.

Installation

npm install redux-bees --save

Or if you use Yarn:

yarn add redux-bees

Usage

Defining your API endpoints

Start by defining the endpoints of your API:

import { buildApi, get, post, patch, destroy } from 'redux-bees';

const apiEndpoints = {
  getPosts:      { method: get,     path: '/posts' },
  getPost:       { method: get,     path: '/posts/:id' },
  createPost:    { method: post,    path: '/posts' },
  updatePost:    { method: patch,   path: '/posts/:id' },
  destroyPost:   { method: destroy, path: '/posts/:id' },
  getCategory:   { method: get,     path: '/categories/:id' },
  getComments:   { method: get,     path: '/posts/:postId/relationships/comments' },
  createComment: { method: post,    path: '/posts/:postId/relationships/comments' },
};

const config = {
  baseUrl: 'https://api.yourservice.com'
};

const api = buildApi(apiEndpoints, config);

You can then perform API requests like this:

api.getPosts()
.then((result) => {
  // {
  //   data: [
  //     {
  //       id: 413,
  //       type: 'posts',
  //       attributes: {
  //         title: 'My awesome post',
  //         ...
  //       }
  //     }
  //   ]
  // }
})
.catch((error) => {
  // {
  //   errors: [
  //     {
  //       status: '404',
  //       title:  'Resource not found',
  //       ...
  //     }
  //   ]
  // }
});

The arguments you need to pass depend on the HTTP method of the request and the presence of placeholders in the path declared for the endpoint:

api.getPost({ id: 12 })
// GET https://api.yourservice.com/posts/12

api.getPost({ id: 12, include: 'comments' })
// GET https://api.yourservice.com/posts/12?include=comments

api.createPost({ data: { type: 'post', attributes: { ... }}})
// POST https://api.yourservice.com/posts

api.updatePost({ id: 12 }, { data: { id: 12, type: 'post', attributes: { ... }}})
// PATCH https://api.yourservice.com/posts/12

api.destroyPost({ id: 12 })
// DELETE https://api.yourservice.com/posts/12

api.getComments({ postId: 12 }
// GET https://api.yourservice.com/posts/12/relationships/comments

api.createComment({ postId: 12 }, { data: { type: 'comment', attributes: { ... }}})
// POST https://api.yourservice.com/posts/12/relationships/comments

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.getPost({ id: 12 })
.then(data => console.log(data));

// This won't produce a new API call

api.getPost({ id: 12 })
.then(data => console.log(data));

Customize headers

By default, API calls will have the following headers setup for you:

Content-Type: application/vnd.api+json
Accept application/vnd.api+json

If you need to pass additional headers, you can use the configureHeaders option:

import store from './store';

const config = {
  baseUrl: 'https://api.yourservice.com'
  configureHeaders(headers) {
    return {
      ...headers,
      'Authorization': `Bearer ${store.getState().session.bearerToken}`,
    };
  },
};

Redux integration

To integrate redux-bees with your Redux store, you need to add a reducer and a middleware:

import {
  createStore,
  applyMiddleware,
  compose,
  combineReducers,
} from 'redux';

import {
  reducer as beesReducer,
  middleware as beesMiddleware,
} from 'redux-bees';

const reducer = combineReducers({
  // ...your other reducers
  bees: beesReducer,
});

const store = createStore(
  reducer,
  applyMiddleware(beesMiddleware())
);

State selectors

This will enable you to dispatch API calls, and get back the result from your Redux state using one of these selectors:

  • getRequestResult(state, apiCall, args)
  • isRequestLoading(state, apiCall, args)
  • hasRequestStarted(state, apiCall, args)
  • getRequestError(state, apiCall, args)

If you want to retrieve all of the above info at once, you can use the following shortcut selector:

  • getRequestInfo(state, apiCall, args)

Example:

import {
  getRequestResult,
  isRequestLoading,
  hasRequestStarted,
  getRequestError,
  getRequestInfo,
} from 'redux-bees';

store.dispatch(api.getPost({ id: 12 }));

getRequestInfo(store.getState(), api.getPost, [{ id: 12 }]);
// {
//   hasStarted: false,
//   isLoading: false,
//   hasFailed: false,
//   result: null,
//   error: null
// }

setTimeout(() => {
  getRequestInfo(store.getState(), api.getPost, [{ id: 12 }]);
  // {
  //   hasStarted: true,
  //   isLoading: false,
  //   hasFailed: false,
  //   result: { id: 12, type: 'post', attributes: { ... } },
  //   error: null
  // }
}, 2000);

The current state of your API calls will be saved in store in the following, normalized form. The bees section of the store should be considered a private area and should be accessed via our state selectors.

store.getState();

// {
//   bees: {
//     requests: {
//       getPosts: {
//         '[]': {
//           isLoading: false,
//           error: null,
//           response: [ { id: '12', type: 'post' } ],
//         }
//       }
//       getPost: {
//         '[ { "id": 12 } ]': {
//           isLoading: false,
//           error: null,
//           response: { id: '12', type: 'post' },
//         }
//       }
//     },
//     entities: {
//       post: {
//         '12': {
//           id: '12',
//           type: 'site',
//           attributes: {
//             name: 'My awesome post',
//             ...
//           }
//         }
//       }
//     }
//   }
// }

React integration

To make it easier to integrate data fetching in your component, you can use a specific higher-order component called query. Basic example of usage:

import React from 'react';
import api from './api';
import { query } from 'redux-bees';

@query('posts', api.getPosts)

export default class App extends React.Component {

  static propTypes = {
    posts: PropTypes.array,
    status: PropTypes.shape({
      posts: PropTypes.shape({
        hasStarted: PropTypes.bool.isRequired,
        isLoading: PropTypes.bool.isRequired,
        hasFailed: PropTypes.bool.isRequired,
        refetch: PropTypes.func.isRequired,
        error: PropTypes.object,
      }),
    }),
  };

  render() {
    const { posts, status } = this.props;

    return (
      <div>
        {
          !status.posts.hasStarted &&
            'Request not started...'
        }
        {
          status.posts.isLoading &&
            'Loading...'
        }
        {
          status.posts.hasFailed &&
            JSON.stringify(status.posts.error)
        }
        {
          posts &&
            JSON.stringify(posts)
        }
      </div>
    );
  }
}

The HOC takes the following ordinal arguments:

  • The name of the prop that will be passed down to the component (ie. 'post');
  • The API call to dispatch (ie. api.getPost);

The HOC will always pass down a status prop, containing all the info about the API request.

If the API call needs parameters (for example, to get a single post), you pass a third argument:

@query('post', api.getPost, (perform, props) => (
  perform({ id: props.match.params.id })
))

The function (perform, props) => perform(...) will be used to actually dispatch the API call with the correct arguments.

You can decorate your component with multiple @query HOCs:

@query('post', api.getPost, (perform, props) => (
  perform({ id: props.match.params.id })
))

@query('comments', api.getComments, (perform, props) => (
  perform({ postId: props.match.params.id })
))

export default class App extends React.Component {
  render() {
    //...
  }
}

In this case, this.props.status.post indicates the status of the api.getPost API call, and this.props.status.comments indicates the status of the api.getComments call.

Dependent data loading

Consider this case:

@query('post', api.getPost, (perform, props) => (
  perform({ id: props.match.params.id })
))

@query('category', api.getCategory, (perform, props) => (
  perform({ id: props.post && props.post.relationships.category.data.id })
))

export default class App extends React.Component {
  render() {
    //...
  }
}

The api.getCategory call cannot be made until we receive the post. redux-bees handles this automatically: the call is only made when props.post && props.post.relationships.category.data.id returns a value. This is because in this API call the id parameter is considered required, as it is indicated with a placeholder:

  ...
  getCategory:   { method: get, path: '/categories/:id' },
  ...

If your API call requires specific parameters in the query string, they can be declared as follows:

  ...
  getPosts:   { method: get, path: '/posts', required: [ 'page' ] },
  ...

Retrieving compound documents

To reduce the number of HTTP requests, JSON API servers may allow responses that include related resources along with the requested primary resources using the include query string.

You can access included entities with the getRelationship selector:

import { query, getRelationship } from 'redux-bees';
import { connect } from 'react-redux';

import api from './api';

@query('post', api.getPost, (perform, props) => (
  perform({ id: props.match.params.id, include: 'categories' })
))

@connect((state, props) => ({
  categories: getRelationship(state, props.post, 'categories')
}))

export default class App extends React.Component {
  render() {
    //...
  }
}

Forced refetch

The status prop contains an refetch() function you can use when you need to force a refetch of data:

import React from 'react';
import api from './api';
import { query } from 'redux-bees';

@query('posts', api.getPosts)

export default class App extends React.Component {
  componentDidMount() {
    const { status } = this.props;

    setTimeout(() => {
      status.posts.refetch();
    }, 2000);
  }

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

    return (
      <div>
        { posts && JSON.stringify(posts) }
      </div>
    );
  }
}

Cache invalidation

After some destructive call (ie. creation of a new post), you often need to invalidate one or more API calls that may have been previously made (ie. the index of posts).

In this case, you can dispatch the invalidateRequests action:

import React from 'react';
import api from './api';
import { query, invalidateRequests } from 'redux-bees';

@query('posts', api.getPosts)

export default class App extends React.Component {

  handleSubmit(attributes) {
    const { dispatch } = this.props;
    
    dispatch(api.createPost({ 
      data: { 
        type: 'post', 
        attributes,
      }
    }))
    .then(() => {
      dispatch(invalidateRequests(api.getPosts));
    });
  }
}

Calling invalidateRequests(api.getPosts) will invalidate every previous API call made to the api.getPosts endpoint. If you just want to invalidate a specific API call, pass the call parameters as second argument:

dispatch(invalidateRequests(api.getPosts, [{ page: '2' }]));

License

ISC

redux-bees's People

Contributors

stefanoverna avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.