...
Code Block | ||
---|---|---|
| ||
// .../api-src/types.ts export interface UserData { ...myUserDataTypeDefinition } export interface SystemData { ...mySystemDataTypeDefinition } export interface SubmitEventData { ...mySubmitEventDataTypeDefinition } |
...
2
...
Add query types for react-query
queries.
Note: react-query does provide exported types here, but in general I found those a bit deeper/more complex than needed for this basic usage. In particular, I am relying on a snake_case upstream data source and converting it into camelCase for consumption. The TS wrapping of the useQuery
calls in this pattern is not optimized for that specific interface, so much as it is for the consuming selectors.
...
language | js |
---|
...
:
...
Define API hooks
Code Block | ||
---|---|---|
| ||
// .../api-src/hooks/api.ts import { useQuery, useMutation } from '@tanstack/react-query'; import { camelCaseObject } from '@edx/frontend-platform'; // or lodash import * as types from '../types'; import { queryKeys } from '../constants'; export const useUserData = (userId: string): types.QueryData<types.UserData> => { const { data, ...status } =( useQuery({ queryKey: [queryKeys.userData, userId], queryFn: () => fetch(`my-urls/user-data/${userId}`), }); return { ...status, data: data ? camelCaseObject(data) : {} }; }.then(camelCaseObject), }) ); export const useSystemData = (): types.QueryData<types.SystemData> => { ( const { data, ...status } = useQuery({ queryKey: [queryKeys.systemData], queryFn: () => fetch('my-urls/system-data'), }); return { ...status, data: data ? camelCaseObject(data) : {} }; }then(camelCaseObject), }); ); export const useSubmit = () => useMutation({ mutationFn: (data: types.SubmitEventData), }); |
...
3: Define Selectors for the API hooks
Code Block | ||
---|---|---|
| ||
// ...api-src/hooks/selectors.ts import * as api from './api'; import * as types from './types'; // generic accessor export const useUserDataStatus = (userId: string): types.QueryStatus => { const { isLoading, isFetching, isInitialLoading, status, error, } = api.useUserData(userId); return { isLoading, isFetching, isInitialLoading, status, error, }; }; export const useIsUserDataLoaded = (userId: string): boolean => ( api.useUserData().status === 'success' ); export const useUserDataResponse = (userId: string): types.UserData => ( api.useUserData().data ); /* * add more, increasingly granular UI selectors here, until all "questions" * asked about the data by the UI for presentation are answered in this module. */ |
...
4: Consume (Profit!)
When consuming data, ALWAYS consume with selectors. The reasoning is that this separates your UI code from the raw details of the incoming data. At all times, the consumers of the data should be asking “Questions” that can be moved around and fixed, even if the incoming data shape changes.
...