-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: Create some stories for the utils/api/* hooks, incl useAggregat…
…eQueryKeys (#84023) In the process of writing the stories i realized that the Parallel and Sequential hooks work differently, and that might be surprising/annoying. One returns data sequentially, the other does not. One can be interrupted/controlled by returning `undefined` from `getQueryKey`, the other cannot. It'd be nice to bring all the features to both. Also `useAggregateQueryKeys` is here. The story could be iterated on to show more of what's happening in the UI instead of referring folks to look at the network traffic. For example, if there were some buttons to fetch more and more `teamIds` on demand that'd be cool. But for now we've got an example wired up, and can see some results. It works as long as the viewer of the story is part of at least 3 teams within their org. <img width="654" alt="SCR-20250124-llev" src="https://github.com/user-attachments/assets/41f0d86d-1edb-410d-b13c-be5de79947b8" /> <img width="662" alt="SCR-20250124-llfz" src="https://github.com/user-attachments/assets/fb22aca0-6866-4087-a157-238b59f58a63" /> <img width="655" alt="SCR-20250124-llho" src="https://github.com/user-attachments/assets/4f616a6e-571b-4fbe-be13-8e019d687645" /> --------- Co-authored-by: Michelle Zhang <[email protected]>
- Loading branch information
1 parent
fd52e4c
commit 8fe530e
Showing
5 changed files
with
241 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import {Fragment, useCallback, useEffect} from 'react'; | ||
|
||
import type {ApiResult} from 'sentry/api'; | ||
import StructuredEventData from 'sentry/components/structuredEventData'; | ||
import storyBook from 'sentry/stories/storyBook'; | ||
import type {Team} from 'sentry/types/organization'; | ||
import useAggregatedQueryKeys from 'sentry/utils/api/useAggregatedQueryKeys'; | ||
import type {ApiQueryKey} from 'sentry/utils/queryClient'; | ||
import useOrganization from 'sentry/utils/useOrganization'; | ||
import {useUserTeams} from 'sentry/utils/useUserTeams'; | ||
|
||
export default storyBook('useAggregatedQueryKeys', story => { | ||
story('useAggregatedQueryKeys', () => { | ||
const organization = useOrganization(); | ||
|
||
// Get a list of valid teamIds for the demo. | ||
const {teams: userTeams} = useUserTeams(); | ||
|
||
const cache = useAggregatedQueryKeys<string, Team[]>({ | ||
getQueryKey: useCallback( | ||
(ids: readonly string[]): ApiQueryKey => { | ||
return [ | ||
`/organizations/${organization.slug}/teams/`, | ||
{ | ||
query: { | ||
query: ids.map(id => `id:${id}`).join(' '), | ||
}, | ||
}, | ||
]; | ||
}, | ||
[organization.slug] | ||
), | ||
onError: useCallback(() => {}, []), | ||
responseReducer: useCallback( | ||
( | ||
prevState: undefined | Team[], | ||
response: ApiResult, | ||
_aggregates: readonly string[] | ||
) => { | ||
return {...prevState, ...response[0]}; | ||
}, | ||
[] | ||
), | ||
bufferLimit: 50, | ||
}); | ||
|
||
useEffect(() => { | ||
// Request only the first team | ||
const firstTeam = userTeams[0]; | ||
if (firstTeam) { | ||
cache.buffer([firstTeam.id]); | ||
} | ||
}, [cache, userTeams]); | ||
useEffect(() => { | ||
// Request some more teams separatly | ||
const moreTeams = userTeams.slice(1, 3); | ||
if (moreTeams.length) { | ||
cache.buffer(moreTeams.map(team => team.id)); | ||
} | ||
}, [cache, userTeams]); | ||
|
||
return ( | ||
<Fragment> | ||
<p> | ||
Checkout the network traffic to really understand how this works. We've called{' '} | ||
<code>cache.buffer()</code> in two independent places, but those 3 ids were | ||
grouped together into one request on the network. | ||
</p> | ||
<StructuredEventData data={cache.data} /> | ||
</Fragment> | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import {Fragment, useCallback} from 'react'; | ||
|
||
import StructuredEventData from 'sentry/components/structuredEventData'; | ||
import storyBook from 'sentry/stories/storyBook'; | ||
import useFetchParallelPages from 'sentry/utils/api/useFetchParallelPages'; | ||
import useOrganization from 'sentry/utils/useOrganization'; | ||
|
||
export default storyBook('useFetchParallelPages', story => { | ||
story('WARNING!', () => ( | ||
<Fragment> | ||
<p> | ||
Using this hook might not be a good idea! | ||
<br /> | ||
Pagination is a good strategy to limit the amount of data that a server needs to | ||
fetch at a given time; it also limits the amount of data that the browser needs to | ||
hold in memory. Loading all data with this hook could cause rate-limiting, memory | ||
exhaustion, slow rendering, and other problems. | ||
</p> | ||
<p> | ||
Before implementing a parallel-fetch you should first think about building new api | ||
endpoints that return just the data you need (in a paginated way), or look at the | ||
feature design itself and make adjustments. | ||
</p> | ||
</Fragment> | ||
)); | ||
|
||
story('useFetchParallelPages', () => { | ||
const organization = useOrganization(); | ||
|
||
const hits = 200; // the maximum number of items we expect to fetch | ||
|
||
const {pages, isFetching} = useFetchParallelPages<{data: unknown}>({ | ||
enabled: true, | ||
hits, | ||
getQueryKey: useCallback( | ||
({cursor, per_page}) => { | ||
return [ | ||
`/organizations/${organization.slug}/projects/`, | ||
{query: {cursor, per_page}}, | ||
]; | ||
}, | ||
[organization.slug] | ||
), | ||
perPage: 20, | ||
}); | ||
|
||
return ( | ||
<Fragment> | ||
<p> | ||
<code>useFetchParallelPages</code> will fetch all pages of data for a given | ||
query. The return value of the hook will update as requests complete, meaning | ||
that the UI can update incrementally. | ||
</p> | ||
<p> | ||
Note that you need to set <code>hits</code> and <code>perPage</code> so the | ||
helper can know how many requests to make. If you don't already know how many | ||
results to expect then it can be helpful to manually request the first full page | ||
of results, check the <code>X-Hits</code> response header, then use the hook to | ||
fetch the complete list of results. Use the same <code>perPage</code> value in | ||
both callsites to leverage the query cache. | ||
</p> | ||
<p> | ||
Note that <code>getQueryKey</code> needs to be a stable reference, so wrap it | ||
with <code>useCallback</code>. | ||
</p> | ||
<StructuredEventData data={{pages: pages.length, isFetching}} /> | ||
</Fragment> | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import {Fragment, useCallback, useRef} from 'react'; | ||
|
||
import StructuredEventData from 'sentry/components/structuredEventData'; | ||
import storyBook from 'sentry/stories/storyBook'; | ||
import useFetchSequentialPages from 'sentry/utils/api/useFetchSequentialPages'; | ||
import useOrganization from 'sentry/utils/useOrganization'; | ||
|
||
export default storyBook('useFetchSequentialPages', story => { | ||
story('WARNING!', () => ( | ||
<Fragment> | ||
<p> | ||
Using this hook might not be a good idea! | ||
<br /> | ||
Pagination is a good strategy to limit the amount of data that a server needs to | ||
fetch at a given time, it also limits the amount of data that the browser needs to | ||
hold in memory. Loading all data with this hook could cause rate-limiting, memory | ||
exhaustion, slow rendering, and other problems. | ||
</p> | ||
<p> | ||
Before implementing a sequential-fetch you should first think about building new | ||
api endpoints that return just the data you need (in a paginated way), or look at | ||
the feature design itself and make adjustments. | ||
</p> | ||
</Fragment> | ||
)); | ||
|
||
story('useFetchSequentialPages', () => { | ||
const organization = useOrganization(); | ||
const {pages, isFetching} = useFetchSequentialPages<{data: unknown}>({ | ||
enabled: true, | ||
initialCursor: undefined, | ||
getQueryKey: useCallback( | ||
({cursor, per_page}) => { | ||
// console.log('cursor', cursor); | ||
return [ | ||
`/organizations/${organization.slug}/projects/`, | ||
{query: {cursor, per_page}}, | ||
]; | ||
}, | ||
[organization.slug] | ||
), | ||
perPage: 20, | ||
}); | ||
|
||
return ( | ||
<Fragment> | ||
<p> | ||
<code>useFetchSequentialPages</code> will fetch all pages of data for a given | ||
query. After all pages are fetched the full list of responses is returned as | ||
`pages`. The UI doesn't incrementally render as data is coming in. | ||
</p> | ||
<p> | ||
Note that <code>getQueryKey</code> needs to be a stable reference, so wrap it | ||
with <code>useCallback</code>. | ||
</p> | ||
<StructuredEventData data={{pages: pages.length, isFetching}} /> | ||
</Fragment> | ||
); | ||
}); | ||
|
||
story('Interrupt a sequential series', () => { | ||
const organization = useOrganization(); | ||
const pagesFetched = useRef(0); | ||
|
||
const {pages, isFetching} = useFetchSequentialPages<{data: unknown}>({ | ||
enabled: true, | ||
initialCursor: undefined, | ||
getQueryKey: useCallback( | ||
({cursor, per_page}) => { | ||
pagesFetched.current++; | ||
if (pagesFetched.current > 2) { | ||
return undefined; | ||
} | ||
|
||
return [ | ||
`/organizations/${organization.slug}/projects/`, | ||
{query: {cursor, per_page}}, | ||
]; | ||
}, | ||
[organization.slug] | ||
), | ||
perPage: 1, | ||
}); | ||
|
||
return ( | ||
<Fragment> | ||
<p> | ||
You can stop a series of requests from continuing by returning a{' '} | ||
<kbd>undefined</kbd> from the <code>getQueryKey</code> callback. | ||
</p> | ||
<p>Here we limit the number of pages to 2</p> | ||
<StructuredEventData data={{pages: pages.length, isFetching}} /> | ||
</Fragment> | ||
); | ||
}); | ||
}); |