Skip to content

React Query Developer's Guide

jchuahtacc edited this page Aug 30, 2021 · 15 revisions

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

tapis-api

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.

Wrapper functions should accept explicitly defined parameters

Each TAPIS function wrapper should have a parameter list with no default values. This means that:

  • Callers must explicitly provide a value
  • If an explicitly provided value will result in an API error, that error should be visible to higher order clients

Wrapper functions should return a Promise

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.

Wrapper functions should use the apiGenerator and errorDecoder utils

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.

tapis-hooks

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

useQuery-based Hooks

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.

useInfiniteQuery-based hooks

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.

useMutation-based hooks

useMutation based hooks should be used for operations that will cause an upstream change in state, such as POST and DELETE. The query keys for operations should explicitly include at least a few required parameters of the operation, to distinguish its identity from other calls to the same endpoint. In the case of useSubmit in the Jobs hooks, appId and appVersion are used as keys.

The wrapper function for the submit utility must accept exactly one parameter. The other parameters for the utility function may be local variables within the hook, such as configuration from useTapisConfig. Generally speaking, this single parameter should be the request object that is required for the call to @tapis/tapis-typescript. It should not be a custom object type that is destructured into further parameters.

Some additional hooks such as useEffect may be used in this hook to provide some state resets to higher order UI components.

The primary mechanism that allows a higher order component to use the mutation is a function that wraps the mutate hook from React Query.