diff --git a/packages/common/README.md b/packages/common/README.md index 5a5dda4ee72..35bef2cc7f8 100644 --- a/packages/common/README.md +++ b/packages/common/README.md @@ -2,6 +2,10 @@ > TODO: description +## Links + +[audius-query docs](./src/audius-query/README.md) + ## Usage ``` diff --git a/packages/common/src/api/collection.ts b/packages/common/src/api/collection.ts index 8bcd3074e12..8888635a51d 100644 --- a/packages/common/src/api/collection.ts +++ b/packages/common/src/api/collection.ts @@ -1,6 +1,5 @@ import { Kind } from 'models' - -import { createApi } from './createApi' +import { createApi } from 'src/audius-query/createApi' const collectionApi = createApi({ reducerPath: 'collectionApi', @@ -24,4 +23,4 @@ const collectionApi = createApi({ }) export const { useGetPlaylistByPermalink } = collectionApi.hooks -export default collectionApi.reducer +export const collectionApiReducer = collectionApi.reducer diff --git a/packages/common/src/api/index.ts b/packages/common/src/api/index.ts index dfda6b572a5..2c31472eec9 100644 --- a/packages/common/src/api/index.ts +++ b/packages/common/src/api/index.ts @@ -1,6 +1,4 @@ -export * from './AudiusQueryContext' export * from './relatedArtists' export * from './track' export * from './collection' export * from './user' -export * from './hooks' diff --git a/packages/common/src/api/reducer.ts b/packages/common/src/api/reducer.ts index b2f613fac00..08766bef8b8 100644 --- a/packages/common/src/api/reducer.ts +++ b/packages/common/src/api/reducer.ts @@ -1,13 +1,13 @@ import { combineReducers } from 'redux' -import collectionApi from './collection' -import relatedArtistsApi from './relatedArtists' -import trackApi from './track' -import userApi from './user' +import { collectionApiReducer } from './collection' +import { relatedArtistsApiReducer } from './relatedArtists' +import { trackApiReducer } from './track' +import { userApiReducer } from './user' export default combineReducers({ - relatedArtistsApi, - trackApi, - collectionApi, - userApi + collectionApi: collectionApiReducer, + relatedArtistsApi: relatedArtistsApiReducer, + trackApi: trackApiReducer, + userApi: userApiReducer }) diff --git a/packages/common/src/api/relatedArtists.ts b/packages/common/src/api/relatedArtists.ts index 12020b7fbc7..51102bb94b1 100644 --- a/packages/common/src/api/relatedArtists.ts +++ b/packages/common/src/api/relatedArtists.ts @@ -1,4 +1,4 @@ -import { createApi } from './createApi' +import { createApi } from 'src/audius-query/createApi' const relatedArtistsApi = createApi({ reducerPath: 'relatedArtistsApi', @@ -17,4 +17,4 @@ const relatedArtistsApi = createApi({ }) export const { useGetRelatedArtists } = relatedArtistsApi.hooks -export default relatedArtistsApi.reducer +export const relatedArtistsApiReducer = relatedArtistsApi.reducer diff --git a/packages/common/src/api/track.ts b/packages/common/src/api/track.ts index 532dcc11a9e..9890837a546 100644 --- a/packages/common/src/api/track.ts +++ b/packages/common/src/api/track.ts @@ -1,8 +1,7 @@ import { Kind } from 'models' +import { createApi } from 'src/audius-query/createApi' import { parseTrackRouteFromPermalink } from 'utils/stringUtils' -import { createApi } from './createApi' - const trackApi = createApi({ reducerPath: 'trackApi', endpoints: { @@ -35,4 +34,4 @@ const trackApi = createApi({ }) export const { useGetTrackById, useGetTrackByPermalink } = trackApi.hooks -export default trackApi.reducer +export const trackApiReducer = trackApi.reducer diff --git a/packages/common/src/api/user.ts b/packages/common/src/api/user.ts index 59ee6d7195e..8eb99793da9 100644 --- a/packages/common/src/api/user.ts +++ b/packages/common/src/api/user.ts @@ -1,6 +1,5 @@ import { Kind } from 'models' - -import { createApi } from './createApi' +import { createApi } from 'src/audius-query/createApi' const userApi = createApi({ reducerPath: 'userApi', @@ -20,4 +19,4 @@ const userApi = createApi({ }) export const { useGetUserById } = userApi.hooks -export default userApi.reducer +export const userApiReducer = userApi.reducer diff --git a/packages/common/src/api/AudiusQueryContext.ts b/packages/common/src/audius-query/AudiusQueryContext.ts similarity index 100% rename from packages/common/src/api/AudiusQueryContext.ts rename to packages/common/src/audius-query/AudiusQueryContext.ts diff --git a/packages/common/src/audius-query/README.md b/packages/common/src/audius-query/README.md new file mode 100644 index 00000000000..a6e887fe086 --- /dev/null +++ b/packages/common/src/audius-query/README.md @@ -0,0 +1,221 @@ +# audius-query + +## Table of Contents + +- [Why audius-query](#why-audius-query) +- [Usage](#usage) + - [Making an Api](#making-an-api) + - [Adding an endpoint](#adding-an-endpoint) + - [Calling the endpoint](#calling-the-endpoint) +- [Cacheing](#cacheing) + - [Endpoint response cacheing](#endpoint-response-cacheing) + - [Entity cacheing](#entity-cacheing) + - [Enable single entity cache hits](#enable-single-entity-cache-hits) +- [Debugging](#debugging) +- [Experimental features](#experimental-features) + - [Pagination (beta)](#pagination-beta) + +## Why audius-query + +- Easy data access pattern +- No need to write sagas, slice, and selectors for each new endpoint +- Integrates with the existing entity cache for `Track`, `Collection`, and `User` + +## Usage + +## Making an api + +1. Call `createApi` which will automatically create a slice with scoped data and status for each endpoint + + ```typescript + const userApi = createApi({ + reducerPath: 'userApi', + endpoints: { + // ADD ENDPOINT DEFINITION HERE + } + }) + + export const { + /* NAMED HOOK EXPORTS */ + } = userApi.hooks + export const userApiReducer = userApi.reducer + ``` + +1. Add the reducer export to [reducer.ts](reducer.ts) + +### Adding an endpoint + +1. Implement the fetch function + + - `audiusClient` and `audiusBackend` are available from the context argument + + ```typescript + endpoints: { + getSomeData: { + fetch: async ( + { id } /* fetch args */, + { apiClient, audiusBackend } /* context */ + ) => { + return await apiClient.getSomeData({ id }) + }, + options: { + // see below + } + } + } + ``` + +1. Endpoint options + + - **`schemaKey`** - the corresponding key in `apiResponseSchema` see [schema.ts](./schema.ts). See [enable entity cachineg on an endpoint](#enable-entity-cacheing-on-an-endpoint) below + + _Note: A schema key is required, though any unreserved key can be used if the data does not contain any of the entities stored in the entity cache (i.e. any of the `Kinds` from [Kind.ts](/packages/common/src/models/Kind.ts))_ + + - **`kind`** - in combination with either `idArgKey` or `permalinkArgKey`, allows local cache hits for single entities. If an entity with the matching `kind` and the `id` or `permalink` exists in cache, we will return that instead of calling the fetch function. See [enable single entity cache hits](#enable-single-entity-cache-hits) below + - **`idArgKey`** - `fetchArgs[idArgKey]` must contain the id of the entity + - **`permalinkArgKey`** - `fetchArgs[permalinkArgKey]` must contain the permalink of the entity + +1. Export hooks + + A Hooks will automatically be generated for each endpoint, using the naming convention `` [`use${capitalize(endpointName)}`] `` (e.g. `getSomeData` -> `useGetSomeData`) + + ```typescript + const userApi = createApi({ + endpoints: { + getSomeData: { + // ... + } + } + }) + + // Export the hook for each endpoint here + export const { useGetSomeData } = userApi.hooks + export default userApi.reducer + ``` + +### Calling the endpoint + +1. Generated fetch hooks take the same args as the fetch function plus an options object. They return the same type returned by the fetch function. + + ```typescript + type QueryHook = ( + fetchArgs: /* matches the first argument to the endpoint fetch fn */ + options: /* {...} */ + ) => { + data: /* return value from fetch function */ + status: Status + errorMessage?: string + } + ``` + +1. In your component + + ```typescript + const { + data: someData, + status, + errorMessage + } = useGetSomeData( + { id: elementId }, + /* optional */ { disabled: !elementId } + ) + + return status === Status.LOADING ? ( + + ) : ( + + ) + ``` + +## Cacheing + +### Endpoint response cacheing + +Each endpoint will keep the lateset response per unique set of `fetchArgs` passed in. This means two separate components calling the same hook with the same arguments will only result in a single remote fetch. However, different endpoints or the same hook with two different arguments passed in will fetch separetly for each. + +### Entity cacheing + +Audius-query uses the `apiResponseSchema` in [schema.ts](./schema.ts) to extract instances of common entity types from fetched data. Any entities found in the response will be extracted and stored individually in the redux entity cache. The cached copy is the source of truth, and the latest cached version will be returned from audius-query hooks. + +### Enable entity cacheing on an endpoint + +In order to enable the automated cacheing behavior for your endpoint, please ensure you follow these steps: + +1. Add the schema for your response structure to `apiResponseSchema` under the appropriate key + + _Note: If an existing key already represents your data's structure, feel free to use it. For example, many endpoints return a list of users. All of them can share the 'users' key in the schema as the structure is identical._ + +2. Ensure that the same key is passed as `schemaKey` to your endpoint's options + +### Enable single-entity cache hits + +We would like hooks like `useGetTrackById({ id: 123 })` which fetch a single entity to be able to hit the cache on their first render. By default, the first call to an audius-query hook with fresh arguments always results in a remote fetch. However for common entities like User, Track, and Collection, it's likely the entity has already been fetched in another part of the app, so we would like to avoid re-fetching them. + +In order to enable these single-entity fetches to hit the local cache, we can provide the `kind` and `idArgKey` options to the endpoint. This tells `createApi` where to look in the cache, as a combination of kind and id are sufficient to look up an entity. + +#### Example (useGetTrackById) + +Here is an example from the `getTrackById` endpoint. Here, the `idArgKey: 'id'` in options corresponds to the `{ id }` argument from the fetch function. This tells us that the arg passed in as `id` to the hook is what we can use to look up the track in the cache. + +So for `useGetTrackById({ id: 123 })` + +- `fetchArgs` is `{ id: 123 }` +- `fetchArgs[idArgKey]` -> `fetchArgs['id']` -> `123` +- because `kind` is `Kind.TRACK` we call `cacheSelectors.getEntity(Kind.TRACK, { id: 123 })` and retrieve the track if it's already there + +```typescript +getTrackById: { + fetch: async ({ id /* matches idArgKey */ }, { apiClient }) => { + return await apiClient.getTrack({ id }) + }, + options: { + idArgKey: 'id', + kind: Kind.TRACKS, + schemaKey: 'track' + } +``` + +## Debugging + +- [createApi.ts](./createApi.ts) contains the implementation of the fetch hooks. You can put breakpoints in `useQuery`. Tip: conditional breakpoints are especially useful since the core logic is shared across every audius-query hook. Try `endpointName === 'myEndpoint && fetchArgs === { ...myArgs }'` to scope down to only your own hook +- Redux debugger - all the data is stored in `state.api['reducerPath']`, and actions are named per endpoint: + - `fetch${capitalize(endpointName)}Loading` + - `fetch${capitalize(endpointName)}Succeeded` + - `fetch${capitalize(endpointName)}Error` + +## Experimental features + +### Pagination (beta) + +see [usePaginatedQuery.ts](./hooks/usePaginatedQuery.ts) + +- `usePaginatedQuery` - wraps an audius-query fetch hook which accepts `{ limit, offset }` and handles pagination with our common `{ hasMore, loadMore }` pattern. Returns the current page of results +- `useAllPaginatedQuery` - the same as `usePaginatedQuery` but returns the cumulative list of results + +Example usage + +```typescript +const { + data: pageOfUsers, + status, + loadMore, + hasMore +} = usePaginatedQuery( + useGetFollowingUsers /* accepts { userId, limit, offset } */, + { userId }, + 10 /* page size */ +) + +return status === Status.LOADING ? ( + +) : ( + +) +``` + +- `hasMore` - true if there are more results available +- `loadMore` - increments the page counter internal to `usePaginatedQuery`, causing the offset to increment and the next page of results to be fetched and returned from the hook diff --git a/packages/common/src/api/createApi.ts b/packages/common/src/audius-query/createApi.ts similarity index 100% rename from packages/common/src/api/createApi.ts rename to packages/common/src/audius-query/createApi.ts diff --git a/packages/common/src/api/hooks/index.ts b/packages/common/src/audius-query/hooks/index.ts similarity index 100% rename from packages/common/src/api/hooks/index.ts rename to packages/common/src/audius-query/hooks/index.ts diff --git a/packages/common/src/api/hooks/usePaginatedQuery.ts b/packages/common/src/audius-query/hooks/usePaginatedQuery.ts similarity index 100% rename from packages/common/src/api/hooks/usePaginatedQuery.ts rename to packages/common/src/audius-query/hooks/usePaginatedQuery.ts diff --git a/packages/common/src/audius-query/index.ts b/packages/common/src/audius-query/index.ts new file mode 100644 index 00000000000..130eaa98cf2 --- /dev/null +++ b/packages/common/src/audius-query/index.ts @@ -0,0 +1,3 @@ +export * from './AudiusQueryContext' +export * from './createApi' +export * from './hooks' diff --git a/packages/common/src/api/schema.ts b/packages/common/src/audius-query/schema.ts similarity index 100% rename from packages/common/src/api/schema.ts rename to packages/common/src/audius-query/schema.ts diff --git a/packages/common/src/api/types.ts b/packages/common/src/audius-query/types.ts similarity index 100% rename from packages/common/src/api/types.ts rename to packages/common/src/audius-query/types.ts diff --git a/packages/common/src/api/utils.ts b/packages/common/src/audius-query/utils.ts similarity index 100% rename from packages/common/src/api/utils.ts rename to packages/common/src/audius-query/utils.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 583a20ea463..22fd3651f4c 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,4 +1,5 @@ export * from './api' +export * from './audius-query' export * from './models' export * from './utils' export * from './services'