Skip to content

Commit

Permalink
[C-2597][C-2651] Add audius-query usage docs (#3427)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Klingler <sliptype@gmail.com>
  • Loading branch information
amendelsohn and sliptype authored May 24, 2023
1 parent 16deebc commit 14034ae
Show file tree
Hide file tree
Showing 17 changed files with 245 additions and 21 deletions.
4 changes: 4 additions & 0 deletions packages/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

> TODO: description
## Links

[audius-query docs](./src/audius-query/README.md)

## Usage

```
Expand Down
5 changes: 2 additions & 3 deletions packages/common/src/api/collection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Kind } from 'models'

import { createApi } from './createApi'
import { createApi } from 'src/audius-query/createApi'

const collectionApi = createApi({
reducerPath: 'collectionApi',
Expand All @@ -24,4 +23,4 @@ const collectionApi = createApi({
})

export const { useGetPlaylistByPermalink } = collectionApi.hooks
export default collectionApi.reducer
export const collectionApiReducer = collectionApi.reducer
2 changes: 0 additions & 2 deletions packages/common/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export * from './AudiusQueryContext'
export * from './relatedArtists'
export * from './track'
export * from './collection'
export * from './user'
export * from './hooks'
16 changes: 8 additions & 8 deletions packages/common/src/api/reducer.ts
Original file line number Diff line number Diff line change
@@ -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
})
4 changes: 2 additions & 2 deletions packages/common/src/api/relatedArtists.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createApi } from './createApi'
import { createApi } from 'src/audius-query/createApi'

const relatedArtistsApi = createApi({
reducerPath: 'relatedArtistsApi',
Expand All @@ -17,4 +17,4 @@ const relatedArtistsApi = createApi({
})

export const { useGetRelatedArtists } = relatedArtistsApi.hooks
export default relatedArtistsApi.reducer
export const relatedArtistsApiReducer = relatedArtistsApi.reducer
5 changes: 2 additions & 3 deletions packages/common/src/api/track.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -35,4 +34,4 @@ const trackApi = createApi({
})

export const { useGetTrackById, useGetTrackByPermalink } = trackApi.hooks
export default trackApi.reducer
export const trackApiReducer = trackApi.reducer
5 changes: 2 additions & 3 deletions packages/common/src/api/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Kind } from 'models'

import { createApi } from './createApi'
import { createApi } from 'src/audius-query/createApi'

const userApi = createApi({
reducerPath: 'userApi',
Expand All @@ -20,4 +19,4 @@ const userApi = createApi({
})

export const { useGetUserById } = userApi.hooks
export default userApi.reducer
export const userApiReducer = userApi.reducer
221 changes: 221 additions & 0 deletions packages/common/src/audius-query/README.md
Original file line number Diff line number Diff line change
@@ -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 ? (
<Loading />
) : (
<DisplayComponent data={someData} />
)
```

## 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 ? (
<Loading />
) : (
<PaginatedUserTable
users={pageOfUsers}
hasMore={hasMore}
loadMore={loadMore}
/>
)
```

- `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
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions packages/common/src/audius-query/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './AudiusQueryContext'
export * from './createApi'
export * from './hooks'
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './api'
export * from './audius-query'
export * from './models'
export * from './utils'
export * from './services'
Expand Down

0 comments on commit 14034ae

Please sign in to comment.