Getting an entity from the API

The apiGetEntity helper action can be used to get a single entity from the API. Examples of this include:

  • Get a user profile
  • Get a recipe
  • Get a post

The utility will handle the following:

  • Executing the API call
  • Store the response entity in the correct location in the entitiesReducer
  • Add metadata to the entity like which API call it came from and when it was requested. This can later be used for caching and debugging, for example.
  • optional Don't perform the request if we already have the data (caching)
  • optional Merge the resulting entity with existing entities
  • optional Automatically update an entityViewReducer such that the new data is immediately displayed on the page.

Example: getting a post

As with the apiRequest actions, we don't use this action directly. Instead, we create a wrapper action creator for each endpoint. The following example gets a COMPONENT entity from the API and uses the slug as the id of the component.

import { POST } from 'common/src/app/data/entityTypes';

export const GET_POST_DETAIL = 'communityActions/GET_POST_DETAIL';

/**
 * Gets a single post entity from the API
 * @param id {string} The ID of the post to get
 */
export const getPostDetail = id => apiGetEntity(
  GET_POST_DETAIL,
  GATEWAY_COMMUNITY_AUTH,
  `/posts/${id}`,
  POST,
  id,
  {
    requestData: {
      includeLastLike: true,
    },
  }
);

Please note the following:

  • First, create a constant string that represents the action type for the resulting redux action. This action type is passed to the first argument of apiGetEntity() can be seen in Redux devtools when the request starts and finishes.
  • As a convention we usually give the same name to the action type as the action creator. The action type is typed in UPPER_CASE and the action creator in camelCase.
  • We don't inline the entity type parameter ("post"). Instead, we use one of the constants defined in the entityTypes module (POST).
  • It is a good practice to document the action creator parameters using JSDoc annotations.
  • Because this is a GET request, the includeLastLike parameter will be added to the query parameters of the request.

Example: executing the call on component initialization

We can use the withInitAction util to execute the above call on component initialization. See Page Initialization for more info.

const addInitAction = withInitAction(
  ['params'],
  ({ params: { postId } }, dispatch) => {
    ...
    return dispatch(getPostDetail(postId));
  },
);

export default compose(
  addInitAction
)(CommunityDetail);

Please note:

  • On API actions, the dispatch() function returns a Promise. Always return that promise to withInitAction
  • If we add withInitAction to a component that is not mounted as a route, we need to call prepareComponent(). See Page Initialization

Example: using the response data

The entity in the API response is automatically stored in the entities reducer. This reducer is a two-dimensional map of entities: first mapped by type, then by id. How we access the entity depends on the scenario:

  1. If we display an entity to the user and it is dynamic which entity it is, we use the entityViewReducer. For example: we display a post but it depends on the URL parameters which post we display.
  2. If we want to use the data in some other way, we can read it directly from the Redux state.

1 Using entityViewReducer

The following example expands on the previous examples by adding a entityViewReducer to display the post on the page.

// create the entityViewReducer in reducers/viewReducer/pagesReducer/communityDetailReducer.js
import combineReducersNamed from 'common/src/app/util/reducers/combineReducersNamed';
import entityViewReducer from 'common/src/app/reducers/view/entityViewReducer';

export default combineReducersNamed({
  post: entityViewReducer,
});
import { makeEntitySelector } from 'common/src/app/reducers/view/entityViewReducer';
...

// Use makeEntitySelector to get the post data to the component
const connector = connect(
  () => {
    const entitySelector = makeEntitySelector();
    return state => ({
      post: entitySelector(state, state.view.pages.communityDetail.post),
    });
  },
);

...

export default compose(
  connector, addInitAction
)(CommunityDetail);

2 Read from state

const connector = connect(
  (state) => {
    // always check if the entity type ("components") exists first
    const footerData = state.entities.components && state.entities.components.footer;

    if (!footerData) {
      return {
        isLoading: true,
      }
    }

    // do something with the data
  }
)(MyComponent);