一个与JSON交互的声明式方式 – redux-bees

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 theconfigureHeadersoption:

import store from './store';

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

Redux integration

To integrateredux-beeswith 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. Thebeessection of the store should be considered a private area and should be accessed via ourstate 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 calledquery. 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 astatusprop, 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@queryHOCs:

@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.postindicates the status of theapi.getPostAPI call, andthis.props.status.commentsindicates the status of theapi.getCommentscall.

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() {
    //...
  }
}

Theapi.getCategorycall cannot be made until we receive thepost.redux-beeshandles this automatically: the call is only made whenprops.post && props.post.relationships.category.data.idreturns a value. This is because in this API call theidparameter 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 theincludequery string.

You can access included entities with thegetRelationshipselector:

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

Thestatusprop contains anrefetch()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 theinvalidateRequestsaction:

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));
    });
  }
}

CallinginvalidateRequests(api.getPosts)will invalidate every previous API call
made to theapi.getPostsendpoint. 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

本站文章除注明转载外,均为本站原创或编译
转载请明显位置注明出处:一个与JSON交互的声明式方式 – redux-bees