Getting a collection from the API

Note: entityLists are deprecated

Getting and storing collections used to be done using "entity lists" (entityListReducer, apiGetEntityList, entityListSelector). These utilities have been deprecated. Because the new utilities are significantly different, the naming has been changed to "collections".

Overview

The apiGetCollection helper action is used to get lists of entities. Examples of this includes:

  • get the list of available groups
  • get a list of all the tags in the community
  • get the recipes that match the search query of the user

The utility will handle the following:

  • Executes the API request
  • Adds the entities in the response to the entities reducer
  • Detects the type and id of each entity based on the passed parameters
  • Adds references to these entities in the collections reducer
  • optional Handles paginated api calls:
    • Stores pagination info about the request in the collections reducer
    • Handles both offset-based and cursor-based pagination
    • Automatically gets the next or previous set of results
  • Adds metadata to each 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 Caching: aborts the request or only request a subset of the collection when we already have (part of) the data.
  • optional Automatically updates a collectionPaginationViewReducer, such that the new data is immediately displayed on the page

General usage

As with other api utils, we do not dispatch apiGetCollection directly. Instead, we create a wrapper action creator for each API endpoint. It looks like this:

// awardActions.js
import { awardsCollectionId } from 'common/src/app/data/collectionIds';

export const GET_AWARDS = 'awardActions/GET_AWARDS';

/**
 * Gets all the awards for a specific user from the API
 * @param [id=me] {string} The id of the user to get awards for
 */
export const getAwards = (id = 'me') => apiGetCollection(
  GET_AWARDS,
  GATEWAY_COMMUNITY_AUTH,
  `/profiles/${id}/awards`,
  awardsCollectionId({ id }),
  {},
  {
    entityType: AWARD,
  }
);
// AwardList/index.js
const addInitAction = withInitAction(
  ['userId'],
  ({ userId }, dispatch) => dispatch(getAwards()),
);

export default compose(
  addInitAction,
)(AwardList);

The possible parameters are explained in more detail below. For now, notice 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 apiGetCollection() 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.
  • It is a good practice to document the action creator parameters using JSDoc annotations.
  • For more info about withInitAction, see Page Initialization

The collections reducer and collection ids

The collectionsReducer is a single reducer that automatically creates new collections when actions for these collections are dispatched. There is no need to create new collection reducers or make modifications to the existing one when implementing new API calls. The collections reducer itself does not contain entities. It only contains references to entities in the entitiesReducer.

Each collection is identified by a unique string. These strings are defined in the collectionIds module. There are two different types of collections: with and without parameters.

Collections without parameters

Ids for collections without parameters are exported as constant strings. For example, the id for the tags in the community are exported like so:

export const COMMUNITY_TAGS = 'communityTags';

These constants can be passed directly to the collectionId parameter of apiGetCollection:

export const getCommunityTags = () => apiGetCollection(
    GET_COMMUNITY_TAGS,
    GATEWAY_CONTENT_AUTH,
    '/community-post-tags',
    COMMUNITY_TAGS,
    ...
);

Collections with parameters

Sometimes a collection is bound by parameters. Some examples of this include:

  • The list of articles on the search page are for a certain search query
  • The list of posts in the Community depends on the applied filters
  • A list of related posts underneath certain post

It is important we use separate collections for each parameter. If we would use a single collection, data for one set of parameters would overwrite data for another. For this reason, the collection id is exported as a function from the collectionIds module:

/**
 * The collection of posts related to a given post
 * @function communityRelatedPostsCollectionId
 * @param params {object}
 * @param params.postId {string} The id of the post
 */
export const communityRelatedPostsCollectionId = parameterizedCollectionId(
  'communityRelatedPosts', ['postId']
);
// usage
communityRelatedPostsCollectionId({ postId: 5 }); // returns "communityRelatedPosts[5]"

Using the collectionSelector

If we want to use a collection in our component, we need to read the references in collectionsReducer and look them up in entitiesReducer. To make this as easy and performant as possible, we use a selector for this called collectionSelector.

// TagList/index.js
import { makeCollectionSelector } from 'common/src/app/reducers/collectionSelector';
...
const connector = connect(
  () => {
    const collectionSelector = makeCollectionSelector();

    return state => ({
      tagDefs: collectionSelector(state, { collectionId: COMMUNITY_TAGS }).entities,
    });
  }
);
...
export default compose(
  connector
)(TagList);

Note that we create a new collectionSelector for each component using makeCollectionSelector. You can read more about this in the reselect documentation

Usage with collectionPaginationView

Sometimes we do not want to show the entire collection to the user. For that reason, a collection can also have "view pagination". This type of pagination is completely separate from the pagination of the collection data itself. For example, the user might be looking at page 1 of the community posts, while we already loaded page 1 though 3 from the API. This "view pagination" can be automatically added to the collectionPaginationViewReducer by apiGetCollection.

In the following example, a view named COMMUNITY_OVERVIEW will automatically update whenever we call getPostOverview(). This will make the community overview page automatically switch to the page that is currently loading when getPostOverview() is called.

import { COMMUNITY_OVERVIEW } from 'common/src/app/data/collectionPaginationViews';
...
export const getPostsOverview = (...) => {
  ...

  return apiGetCollection(
    GET_POSTS_OVERVIEW,
    GATEWAY_COMMUNITY_AUTH,
    '/posts',
    communityPostsCollectionId({ tags, onlyMe, sort }),
    requestPagination,
    {
      updatePaginationView: COMMUNITY_OVERVIEW,
      requestData,
      entityType: POST,
    }
  );
};

Infinite scrolling

When displaying a collection with infinite scrolling, we do not want to switch the pagination every time we load a new page. Instead, we want to extend the entities that are currently visible with the entities that are loading from the API. We can achieve this by setting the extend parameter to true. The following example is the same as the previous, with an extend parameter added:

import { COMMUNITY_OVERVIEW } from 'common/src/app/data/collectionPaginationViews';
...
export const getPostsOverview = (...) => {
  ...

  return apiGetCollection(
    GET_POSTS_OVERVIEW,
    GATEWAY_COMMUNITY_AUTH,
    '/posts',
    communityPostsCollectionId({ tags, onlyMe, sort }),
    requestPagination,
    {
      updatePaginationView: {
        target: COMMUNITY_OVERVIEW,
        extend: true,
      },
      requestData,
      entityType: POST,
    }
  );
};

Using a collectionPaginationView in a component

We can connect with collectionPaginationView using the collectionPaginationViewSelector. This selector will return which entities should be currently visible according to the pagination and the data in the collectionsReducer.

// CommunityOverview/index.js

const connector = connect(
  () => {
    const collectionPaginationViewSelector = makeCollectionPaginationViewSelector(
      COMMUNITY_OVERVIEW
    );

    return (state) => {
      const posts = collectionPaginationViewSelector(state);

      return {
        posts: posts.entities,
        pagination: posts.pagination,
      };
    };
  },
);
...

export default compose(
  connector,
)(CommunityOverview);

Handling caching

Caching in apiGetCollection is handled by the cache parameter. This parameter is a function that determines if a request needs to be made or if we can use existing data. By default, this is set to collectionCachingDefault. If you want to write a custom caching function, see the api docs for GetCollectionCachingCallback