React Query
Tramvai provides a complete integration for the awesome @tanstack/react-query library. React Query is a perfect solution for data fetching which can significantly improve DX and UX in your application.
Currently supported @tanstack/react-query version is ^4.7.1
Getting Started
Installation
You need to install @tramvai/react-query
and @tramvai/module-react-query
packages
npx tramvai add @tramvai/react-query
npx tramvai add @tramvai/module-react-query
And connect module to the project
import { createApp } from '@tramvai/core';
import { ReactQueryModule } from '@tramvai/module-react-query';
createApp({
name: 'tincoin',
modules: [ReactQueryModule],
});
Quick Start
⌛ Create a new query:
import { createQuery } from '@tramvai/react-query';
// @tramvai/module-http-client needed
import { HTTP_CLIENT } from '@tramvai/tokens-http-client';
const query = createQuery({
key: ['repoData'],
fn() {
return this.deps.httpClient
.get<Record<string, any>>('https://api.github.com/repos/tinkoff/tramvai')
.then(({ payload }) => payload);
},
deps: {
httpClient: HTTP_CLIENT,
},
});
⌛ Use query in component:
import { useQuery } from '@tramvai/react-query';
const Example = () => {
const { isLoading, error, data } = useQuery(query);
if (isLoading) {
return 'Loading...';
}
if (error) {
return `An error has occurred: ${error.message}`;
}
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong> <strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
);
};
Configuration
You can configure options for the QueryClient through the QUERY_CLIENT_DEFAULT_OPTIONS_TOKEN
, e.g.:
import { provide } from '@tramvai/core';
import { QUERY_CLIENT_DEFAULT_OPTIONS_TOKEN } from '@tramvai/tokens-react-query';
const provider = provide({
provide: QUERY_CLIENT_DEFAULT_OPTIONS_TOKEN,
useValue: {},
});
Default Options
Here is QUERY_CLIENT_DEFAULT_OPTIONS_TOKEN
default value:
const defaults = {
queries: {
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
},
};
refetchOnMount
option is disabled because usually you want to prefetch query for page, and no needs to fetch it again on mount. Another options are disabled for better network requests control from the application side.
Also, default retries is disabled for actions conditions errors, because this errors are expected and retrying is meaningless.
Examples
You can find and run examples in our repo
Basic Query
import { createQuery, useQuery } from '@tramvai/react-query';
const query = createQuery({
key: ['base'],
async fn() {
await sleep(100);
return 'Response from API';
},
});
export function Component() {
const { data, isLoading } = useQuery(query);
return <div>{isLoading ? 'loading...' : data}</div>;
}
Prefetching
Always use page component actions
static property with prefetchAction to run query for this route at server-side
import { createQuery, useQuery } from '@tramvai/react-query';
const query = createQuery({
key: ['base'],
async fn() {
await sleep(100);
return 'Response from API';
},
});
export default function Page() {
const { data, isLoading } = useQuery(query);
return <div>{isLoading ? 'loading...' : data}</div>;
}
Page.actions = [query.prefetchAction()];
SPA and dynamic url parameters
For example, you have route like /blog/:article/
, and a few specific urls on it - /blog/first/
and /blog/second/
. From any article you can made SPA-transition to another article.
This case can be a little bit tricky for data prefetching:
- for
prefetchAction
, you can't pass slug, and need to resolve it fromPAGE_SERVICE_TOKEN
inkey
andfn
- for SPA-transitions on the same route, you need to pass slug to
useQuery
in component, and can resolve it fromuseRoute
hook.
Complete example:
import { createQuery, useQuery } from '@tramvai/react-query';
import { useRoute, PAGE_SERVICE_TOKEN } from '@tramvai/module-router';
const query = createQuery({
key: (slug?: string) => {
// get slug from query options, otherwise from page service
const articleId = slug || this.deps.pageService.getCurrentRoute().params.article;
// it is important, any dynamic part of query need to be saved in key
return ['article', articleId];
},
actionNamePostfix: 'getArticleById',
async fn(slug?: string) {
// get slug from query options, otherwise from page service
const articleId = slug || this.deps.pageService.getCurrentRoute().params.article;
return this.deps.apiService.fetchArticle(articleId);
},
deps: {
apiService: API_SERVICE_TOKEN,
pageService: PAGE_SERVICE_TOKEN,
},
});
export default function Article() {
// the same data as `pageService.getCurrentRoute()` with routes changes subscription
const route = useRoute();
// after SPA-transitions, this query will be executed client-side, always with fresh slug
const { data, isLoading } = useQuery(query, route.params.article);
return <div>{isLoading ? 'loading...' : data}</div>;
}
// during the first page load, this query will be executed server-side, with actual slug from page service
Article.actions = [query.prefetchAction()];
Dependencies
Queries has full Dependency Injection support, so you can declare dependencies like in DI providers, in deps
property. These dependencies will be available in the action fn
and key
functions фы this.deps
property.
When you want to prefetch query with specific parameter, the only way to do it right is to get this parameter from DI
import { createQuery, useQuery } from '@tramvai/react-query';
import { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
import { HTTP_CLIENT } from '@tramvai/tokens-http-client';
const query = createQuery({
key() {
return `base/${this.deps.pageService.getCurrentUrl().query.route}`;
},
actionNamePostfix: 'requestByRoute',
async fn() {
const { apiClient, pageService } = this.deps;
const { payload } = await httpClient.get<string>('api/by-route', {
query: {
route: pageService.getCurrentUrl().query.route ?? 'test',
},
});
return payload;
},
deps: {
httpClient: HTTP_CLIENT,
pageService: PAGE_SERVICE_TOKEN,
},
});
// eslint-disable-next-line import/no-default-export
export default function Component() {
const { data, isLoading } = useQuery(query);
return <div>{isLoading ? 'loading...' : data}</div>;
}
Shared Query
If you want to use same query in different components, this queries requests will be deduplicated, and response data will be shared.
import { useState, useEffect } from 'react';
import { createQuery, useQuery } from '@tramvai/react-query';
const query = createQuery({
key: ['base'],
async fn() {
await sleep(100);
return 'Response from API';
},
});
const Child1 = () => {
const { isLoading, data } = useQuery(query);
return <div>Child1: {isLoading ? 'loading...' : data}</div>;
};
const Child2 = () => {
const { isLoading, data } = useQuery(query);
return <div>Child2: {isLoading ? 'loading...' : data}</div>;
};
export function Component() {
return (
<>
<Child1 />
<Child2 />
</>
);
}
Request parameters
Parameter will be available in key
and fn
functions as first argument. You can pass this parameter as second argument of useQuery
hook.
Query keys always should include specific parameters, on which the result of request depends
import { createQuery, useQuery } from '@tramvai/react-query';
const query = createQuery({
key: (parameter: string) => ['api-group', parameter],
actionNamePostfix: 'requestGroup',
async fn(parameter) {
await sleep(100);
return `Response ${parameter} from API`;
},
});
const Child1 = () => {
const { isLoading, data } = useQuery(query, 'test-1');
return <div>Child1: {isLoading ? 'loading...' : data}</div>;
};
const Child2 = () => {
const { isLoading, data } = useQuery(query, 'test-2');
return <div>Child2: {isLoading ? 'loading...' : data}</div>;
};
export function Component() {
return (
<>
<Child1 />
<Child2 />
</>
);
}
Custom Query options
Default options will be used for all queries, but you can rewrite them for specific query.
import { createQuery, useQuery } from '@tramvai/react-query';
const query = createQuery({
key: 'time',
async fn() {
await sleep(100);
return 'Response from API';
},
// default options for this query
queryOptions: {
refetchOnWindowFocus: true,
refetchOnMount: true,
},
});
export function Component() {
const { data } = useQuery(
// the same query with rewritten options
query.fork({
refetchInterval: 2000,
refetchIntervalInBackground: false,
})
);
return <div>{data}</div>;
}
Failed requests
React Query will retry failed requests 3 times by default with exponential backoff. You can change this behavior with queryOptions
property.
import { createQuery, useQuery } from '@tramvai/react-query';
const query = createQuery({
key: 'base',
async fn() {
throw Error('Error from API');
},
queryOptions: {
retry: 2,
retryDelay: 500,
},
});
export function Component() {
const { data, isLoading, isError, error } = useQuery(query);
if (isLoading) {
return <div>loading...</div>;
}
if (isError) {
return <div>error: {error!.message}</div>;
}
return <div>{data}</div>;
}
Conditions
All actions retrictions are supported:
import { createQuery, useQuery } from '@tramvai/react-query';
import { FAKE_API_CLIENT } from '../../fakeApiClient';
const query = createQuery({
key: 'base',
async fn() {
await sleep(1000);
return 'Some slow response from API';
},
conditions: {
onlyServer: true,
},
});
export function Component() {
const { data = 'no-data', isLoading } = useQuery(query);
return <div>{isLoading ? 'loading...' : data}</div>;
}
Infinite Query
import { createInfiniteQuery, useInfiniteQuery } from '@tramvai/react-query';
import { HTTP_CLIENT } from '@tramvai/tokens-http-client';
interface Response {
nextPage?: number;
list: string[];
}
const query = createInfiniteQuery({
key: 'list',
async fn(_, start = 0) {
const { payload } = await this.deps.httpClient.get<Response>('api/list', {
query: {
count: 30,
start,
},
});
return payload;
},
getNextPageParam: (page: Response) => {
return page.nextPage;
},
deps: {
httpClient: HTTP_CLIENT,
},
infiniteQueryOptions: {},
});
export function Component() {
const { data, isLoading, fetchNextPage, hasNextPage } = useInfiniteQuery(query);
if (isLoading) {
return <>loading...</>;
}
return (
<div>
<div>
{data!.pages.map((page) => {
return page.list.map((entry) => {
return <div key={entry}>{entry}</div>;
});
})}
</div>
{hasNextPage && (
<button type="button" onClick={() => fetchNextPage()}>
Load more
</button>
)}
</div>
);
}
Mutation
import { createMutation, useMutation } from '@tramvai/react-query';
import { HTTP_CLIENT } from '@tramvai/tokens-http-client';
const mutation = createMutation({
key: 'post',
async fn(_, data: string) {
const { payload } = await this.deps.httpClient.post('api/post', {
body: {
data,
},
});
return payload;
},
deps: {
httpClient: HTTP_CLIENT,
},
});
export function Component() {
const { isLoading, mutate } = useMutation(mutation);
if (isLoading) {
return <>loading...</>;
}
return (
<button type="button" onClick={() => mutate('test')}>
Send data
</button>
);
}
AbortController
Similar to Actions AbortController, abortSignal
and abortController
for current execution context available in fn
context (this
):
const query = createQuery({
key: ['abortable'],
async fn() {
const signal = this.abortSignal;
return this.deps.httpClient.request({ path: '/api', signal });
},
deps: {
httpClient: HTTP_CLIENT_TOKEN,
},
});
This signal has subscription to react-query cancellation signal, see example in use-query-cancel
example
Use @tanstack/react-query
directly
Prefer to use methods from the @tramvai/react-query
as it can work both with the Query
wrapper and the query options to @tanstack/react-query
itself
You can get QueryClient
from di by token QUERY_CLIENT_TOKEN
or using method useQueryClient
in React-components.
To convert wrapped Query
object to object acceptable by @tanstack/react-query
use method raw of the Query
instance.
API Reference
Query
A wrapper around react-query options with tramvai integration.
fork
Create new Query
from an existing query with option to override settings.
import { createQuery } from '@tramvai/react-query';
const query = createQuery();
const newQuery = query.fork({
refetchInterval: 2000,
refetchIntervalInBackground: false,
});
prefetchAction
Return a tramvai action that can be used to prefetch current query
export default function PageComponent() {
const { data, isLoading } = useQuery(query);
return <div>{isLoading ? 'loading...' : data}</div>;
}
PageComponent.actions = [query.prefetchAction()];
fetchAction
Return a tramvai action that can be used to fetch current query
const action = declareAction({
name: 'action',
async fn() {
const result = await this.executeAction(query.fetchAction());
console.log('__action__', result);
},
});
raw
Might be used when the raw query options is needed. The result can be passed to the underlying methods of @tanstack/react-query
lib in cases when @tramvai/react-query
doesn't provide appropriate wrapper. This method is used internally in the lib to redirect calls to @tanstack/react-query
.
createQuery
Allows you to create a Query
object that can later be used in components using useQuery
. Used to execute single data retrieval requests.
import { createQuery } from '@tramvai/react-query';
const query = createQuery({
key: ['base'],
fn: async (_, { apiClient }) => {
const { payload } = await apiClient.get('api/base');
return payload;
},
deps: {
apiClient: TINKOFF_API_SERVICE,
},
});
Unique query parameters
To create a generic query
that takes parameters for a query, you must return a unique key
, you can read more about this in the official documentation section Query Keys
As a parameter key
you can use:
- a string, such as
key: 'query-name'
- an array where any serializable data can be used as elements, for example
key: ['query-name', false, { bar: 'baz }]
- a function that takes the parameters with which
query
is called and returns a string -key: (this: { deps }, options) => 'query-name'
. Where throughthis.deps
you can get resolved deps for the query. - a function that accepts parameters, with which
query
is called, and returns an array, where any serializable data can be used as elements -key: (this: { deps }, options) => ['query-name', options, { bar: 'baz' }]
If you pass parameter key
as a function, you should pass actionNamePostfix to avoid duplicates in the server-client cache
import { createQuery, useQuery } from '@tramvai/react-query';
const query = createQuery({
key: (id: number) => ['user', id],
actionNamePostfix: 'requestUser',
async fn(id) {
const { apiClient } = this.deps;
const { payload } = await apiClient.get(`api/user/${id}`);
return payload;
},
deps: {
apiClient: TINKOFF_API_SERVICE,
},
});
export function Component({ id }) {
const { data, isLoading } = useQuery(query, id);
return <div>{isLoading ? 'loading...' : data}</div>;
}
useQuery
React hook for working with Query
object
import { useQuery } from '@tramvai/react-query';
export function Component() {
const { data, isLoading } = useQuery(query);
return <div>{isLoading ? 'loading...' : data}</div>;
}
useQueries
React Hook for working with the list of Query
objects
import { useQueries } from '@tramvai/react-query';
export function Component() {
const [{ data: data1, isLoading: isLoading1 }, { data: data2, isLoading: isLoading2 }] =
useQueries([query1, query2]);
return (
<div>
<div>{isLoading1 ? 'loading1...' : data1}</div>
<div>{isLoading2 ? 'loading2...' : data2}</div>
</div>
);
}
createInfiniteQuery
Creates an InfiniteQuery
object that can later be used in components using useInfiniteQuery
. It is used to execute queries to obtain a sequence of data that can be loaded as the component runs.
import { createInfiniteQuery } from '@tramvai/react-query';
const query = createInfiniteQuery({
key: 'list',
async fn(_, start = 0) {
const { apiClient } = this.deps;
const { payload } = await apiClient.get<Response>('api/list', {
query: {
count: 30,
start,
},
});
return payload;
},
getNextPageParam: (page: Response) => {
return page.nextPage;
},
deps: {
apiClient: TINKOFF_API_SERVICE,
},
});
useInfiniteQuery
React hook for working with the InfiniteQuery
object
import { useInfiniteQuery } from '@tramvai/react-query';
export function Component() {
const { data, isLoading, fetchNextPage, hasNextPage } = useInfiniteQuery(query);
if (isLoading) {
return 'loading...';
}
return (
<div>
<div>
{data.pages.map((page) => {
return page.list.map((entry) => {
return <div key={entry}>{entry}</div>;
});
})}
</div>
{hasNextPage && (
<button type="button" onClick={() => fetchNextPage()}>
Load more
</button>
)}
</div>
);
}
createMutation
Creates a Mutation
object that can later be used in components using useMutation
. Used to send and modify data in the api.
import { createMutation } from '@tramvai/react-query';
const mutation = createMutation({
key: 'post',
async fn(_, data: string) {
const { apiClient } = this.deps;
const { payload } = await apiClient.post('api/post', {
body: {
data,
},
});
return payload;
},
deps: {
apiClient: TINKOFF_API_SERVICE,
},
});
useMutation
React hook for working with the Mutation
object
import { useMutation } from '@tramvai/react-query';
export function Component() {
const { isLoading, mutate } = useMutation(mutation);
if (isLoading) {
return 'loading...';
}
return (
<button type="button" onClick={() => mutate('test')}>
Send data
</button>
);
}