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
entitiesreducer - Detects the
typeandidof each entity based on the passed parameters - Adds references to these entities in the
collectionsreducer - optional Handles paginated api calls:
- Stores pagination info about the request in the
collectionsreducer - Handles both offset-based and cursor-based pagination
- Automatically gets the next or previous set of results
- Stores pagination info about the request in the
- 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