-
Notifications
You must be signed in to change notification settings - Fork 16
React Query Developer's Guide
The React-Query Milestone replaces the tapis-redux
middleware for managing the lifecycle of API requests with the react-query
hooks library. The advantages of using this library are:
- Significantly reduction in boilerplate code, compared to using
react-redux-saga
- Opinionated, hooks-based method for performing results caching, pagination, re-fetching
- Least-concerns approach for UI components tracking their own state
In addition, this milestone will include other refactors for improved code maintainability:
- Use of the
create-react-app
boilerplate, with more strict Typescript type-checking, better hot-loader configuration and test watcher - Removal of external configuration and callback hooks on
tapis-ui
components, to simplify development (now that external webcomponents is no longer a primary objective of this project) - Standardized
tapis-ui
component wrappers to simplify development of new components and provide consistency (soon to be implemented) - Removed
src
folder structure and integrated tests, for simplified code management
Calls to tapis-typescript
are now wrapped in utility functions contained in the tapis-api
module. The exports of these functions are promises on the results that also provide error json decoding, should a standard TAPIS 4-stanza response be provided on a failed API call. As an example, we will look at the structure of the login
function for the Authenticator
module.
Each TAPIS function wrapper should have one parameter, that is an object containing fields for values. This allows adaptation for React-Query's useMutation
or useQuery
hooks. (This should be consistent for all tapis-api
wrapper functions, and should be corrected in #126). The dictionary type should be defined as an interface. See LoginParams
.
Each TAPIS function wrapper should be declared to return a Promise of the wrapped function return type. See 'login'. In the case of the login
function, the createToken
function returns the RespCreateToken
type, so the login
wrapper should return a Promise of that type.
To avoid boilerplate and provide consistency between wrappers, the apiGenerator
and errorDecoder
generic utility functions should be used.
apiGenerator
generates an API objects that have an injected module configuration. The call to apiGenerator
requires the tapis-typescript
modules as parameters, including a basePath
tenant URL configuration as well as an optional jwt
token.
errorDecoder
provides promise based handling of any errors generated by tapis-typescript
calls. The call to errorDecoder
should accept an arrow function as its only parameter. The arrow function should return the result of the tapis-typescript
promise function. If the result is a JSON error, it will automatically be decoded.
This module should provide one hook per tapis-typescript
operation, organized by TAPIS service. Each hook will return react-query
hook results that invoke calls to tapis-api
functions. There are three types of react-query
hooks currently in use:
-
useQuery
, for when a limited number of results are returned by an API call -
useInfiniteQuery
, for when a utility function requires pagination of results -
useMutation
, for when a utility function sends a POST-like operation that is not subject to automatic re-querying
tapis-hooks/systems/useList
is an example of a hook that uses useQuery
.
It first makes a call to the useTapisConfig
hook to retrieve the configured TAPIS basePath and current JWT. This follows react-query
Dependent Query pattern, in that if the TAPIS configuration changes (such as a user logging in or logging out) the useQuery
hook will automatically re-run the query.
The call to react-query's useQuery
hook requires a unique set of values as a key to the useQuery
call. This provides react-query
a way of differentiating between cached requests. The three parameters seen here are:
- A string from
queryKeys.ts
in the same namespace - The request parameter object to differentiate between requests to the same endpoint with different parameters
- The
accessToken
associated with the user making the request.
When a client component calls the useList
hook, they can expect that the data returned by the hook will be automatically refreshed (as part of React Query's feature set) and cached to prevent duplicate calls.
The function that makes the request is an arrow function wraps a call to the list
function. This pattern is frequently used to pass parameters to the utility function from within the hook that are derived from other hooks (such as useTapisConfig
).
Lastly, the enabled
option for React-Query allows the hook to prevent unnecessary calls that would fail, such as before authentication occurs.
The useList
hook for the Files service uses React Query's useInfiniteQuery
hook, which provides pagination capabilities. This allows a downstream client such as FilesListing
to use the fetchNextPage
function returned by the hook, thus facilitating pagination.
The arrow function that wraps the listing call is passed the pageParams
object by React-Query whenever a page is fetched. (In this line of code, we provide it a default value generated from the hook parameters for its retrieval of the first page of results.) useInfiniteQuery
requires the getNextPageParam
function to determine how to calculate the next page parameters for TAPIS (in this case, the offset
value being important for pagination in TAPIS.) Note: This function will be consistent between all paginated listings for TAPIS and should eventually be refactored into a utility function.
The data
object returned by useInfiniteQuery
differs from useQuery
in that the results are part of a page array. For some views, it may be more useful to simply receive all paginated results in one array for UI use cases like infinite scrolling. Therefore, this hook also returns a concatenatedResults
array with all pages reduced into one array. A downstream client such as FilesListing
in tapis-ui
can access this array to treat all results as one aggregated listing.