Creating React Query Service
Generator
Generator code (opens in a new tab)
nx generate @brainly-gene/tools:service --name=post --directory=my-blog/services --serviceType=react-query --tags=domain:my-blog
After running this command you will be additionally prompted to provide the CRUD operations for the service:
> NX Generating @brainly-gene/tools:service
✔ What is the service name? (the name should be an entity name in singular form - for example post, user, book, etc.) · post
✔ What is the service directory? · my-blog/services
✔ What are the service tags? · domain:my-blog
✔ What is the service type? · react-query
? Select CRUD functions you want to generate
◉ usePosts - to get multiple posts
◯ useCreatePost - to create a new post
◉ useUpdatePost - to update a single post
◯ useDeletePost - to delete a single post
❯◉ usePost - to get a single post
After selecting desired CRUD operations, the generator will create the following files:
CREATE libs/my-blog/services/post-service/project.json
CREATE libs/my-blog/services/post-service/.eslintrc.json
CREATE libs/my-blog/services/post-service/.babelrc
CREATE libs/my-blog/services/post-service/src/index.ts
CREATE libs/my-blog/services/post-service/tsconfig.lib.json
CREATE libs/my-blog/services/post-service/tsconfig.json
CREATE libs/my-blog/services/post-service/jest.config.ts
CREATE libs/my-blog/services/post-service/tsconfig.spec.json
UPDATE tsconfig.base.json
CREATE libs/my-blog/services/post-service/src/README.md
CREATE libs/my-blog/services/post-service/src/lib/useUpdatePost.ts
CREATE libs/my-blog/services/post-service/src/lib/usePost.ts
CREATE libs/my-blog/services/post-service/src/lib/usePosts.ts
This generator creates a React Query service in the specified directory with the provided CRUD operations. Each service hook comes with example API calls and types:
// Change this to match output of your API
export type PostsAPIType = {
id: number;
userId: number;
title: string;
body: string;
}[];
// Change this to match input of your API
export type VariablesType = {
userId: number;
};
export const queryKey = (variables?: VariablesType) => [
'get-posts-key',
variables,
];
export function defaultQueryFn(
variables: VariablesType,
context?: QueryFunctionContext
) {
const url = `https://jsonplaceholder.typicode.com/posts?userId=${variables.userId}`;
const fetchMethod = typeof window === 'undefined' ? nodeFetch : fetch;
return reactQueryFetchWrapper<PostsAPIType>(() => fetchMethod(url));
}
// Use this function to run this query on SSR, pass the subapp as queryFn
export async function queryPosts(
client: QueryClient,
variables: VariablesType,
queryFn = defaultQueryFn
) {
return client.fetchQuery({
queryFn: () => queryFn(variables),
queryKey: queryKey(variables),
});
}
export function usePosts(props: { variables: VariablesType }) {
const queryClient = useInjectedReactQueryClient();
// useInfiniteQuery if paging is needed
const result = useQuery(
{
queryKey: queryKey(props.variables),
queryFn: (ctx) => defaultQueryFn(props.variables, ctx),
},
queryClient
);
return transformReactQueryResponse(result);
}
Usage in a Module
import { usePosts } from '@acme/my-blog/services/post-service';
function MyModule() {
const { data, error, loading } = usePosts({
variables: {
userId: 1,
},
});
if (loading) {
return <Spinner />;
}
if (error) {
return <MyErrorUi message={error} />;
}
return <MyUIComponent data={data} />;
}
SSR Hydration
Next.js
To hydrate your query using server-side rendering, run the query on the page in getServerSideProps
, then dehydrate and hydrate the client state:
Example page with SSR:
import { compose } from 'ramda';
import { getRequestHeaders } from '@brainly-gene/next';
import { getHomePageContainer } from '../ioc/getHomePageIoc';
import { GetServerSideProps } from 'next/types';
import { withIoc, reactQueryFactory } from '@brainly-gene/core';
import { QueryClient } from '@tanstack/react-query';
import { queryPosts } from '@acme/my-blog/services/post-service';
import { getPosts } from '@acme/my-blog/api/post-api';
function HomePage() {
return <div>Hello ExamplePage!</div>;
}
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const reactQueryClient = reactQueryFactory(() => new QueryClient());
const reactQueryClientInstance = reactQueryClient.getClient();
const reqHeaders = getRequestHeaders(req);
// Invoke service queries here:
await queryPosts(reactQueryClientInstance, { userId: 1 });
// You can also pass custom query function to fetch the data directly,
// without calling external endpoint
await queryPosts(reactQueryClientInstance, { userId: 1 }, () => {
return getPosts({ userId: 1 });
});
// IF you have multiple queries, you can (and you should) run them in parallel
await Promise.all([
queryPosts(),
queryFunnyCats()
]);
// End of queries invokes
return {
props: {
dehydratedQueryClient: reactQueryClient.dehydrate(),
reqHeaders,
},
};
};
export default compose(withIoc(getHomePageContainer))(HomePage);
The variables passed to queryPosts
must be consistent between the
server and client. Cache keys depend on these elements. If they differ between
client and server, hydration will not work and the query will be re-executed on the client.
Query client is hydrated in the IOC container:
import { Container } from 'inversify';
import { getBaseContainer } from './baseIoc';
import { HomePagePropsType } from '../types/types';
import { ServiceTypes } from '@brainly-gene/core';
import { Factory, factory } from '@brainly-gene/core';
import { ReactQueryClientType } from '@brainly-gene/core';
export function getHomePageContainer(props?: HomePagePropsType) {
const baseContainer = getBaseContainer();
const queryClient =
baseContainer.get<ReactQueryClientType>('reactQueryClient');
queryClient.hydrate(props?.dehydratedQueryClient);
const clients = {
[ServiceTypes.reactQuery]: () => {
return queryClient.getClient();
},
};
const container = new Container();
container.parent = baseContainer;
container.bind<Factory>('serviceFactory').toFunction(factory(clients));
return container;
}
Pagination
To implement pagination, use useInfiniteQuery
instead of useQuery
. Here is an example of how to implement pagination:
export type CharactersAPIType = {
info: {
count: number;
pages: number;
next: string | null;
prev: string | null;
};
results: Character[];
};
type Character = {
id: number;
name: string;
};
export type VariablesType = {};
export const queryKey = (variables?: VariablesType) => [
'get-characters-key',
variables,
];
export function defaultQueryFn(
variables: VariablesType,
context?: QueryFunctionContext
) {
const url = context.pageParam as string; // use the pageParam to get the next page
const fetchMethod = typeof window === 'undefined' ? nodeFetch : fetch;
return reactQueryFetchWrapper<CharactersAPIType>(() => fetchMethod(url));
}
export function useCharacters(props: { variables: VariablesType }) {
const queryClient = useInjectedReactQueryClient();
const result = useInfiniteQuery( // useInfiniteQuery instead of useQuery
{
queryKey: queryKey(props.variables),
queryFn: (ctx) => defaultQueryFn(props.variables, ctx),
initialPageParam: 'https://rickandmortyapi.com/api/character?page=1', // set the initial page param
getNextPageParam: (lastPage) => {
// get the next page param based on the last page response,
// this can be page number, cursor or whole URL
return lastPage.info.next;
},
},
queryClient
);
return transformReactQueryResponse(result);
}
Fetch on demand
You can fetch data on demand by utilizing enabled
property of useQuery
or useInfiniteQuery
hooks:
export function useTodos(props: { variables: VariablesType }, skip = false) {
const queryClient = useInjectedReactQueryClient();
// useInfiniteQuery if paging is needed
const result = useQuery(
{
queryKey: getTodosQueryKey(props.variables),
queryFn: (ctx) => defaultQueryFn(props.variables, ctx),
enabled: !skip,
},
queryClient
);
return transformReactQueryResponse(result);
}
Crud Operations and strategies for data synchronization
- Optimistic Updates: Provide a simulated response (
optimisticResponse
) for immediate visual feedback in the UI, assuming the mutation will succeed. - Manual Cache Updates (
updates
): Directly modify the cache after a mutation using the provided update functions. - Automated Query Refetching (
refetchQueries
): Specify which queries to rerun after a successful mutation for automatic data sync.
Practical Examples
Modifying an Existing TODO Item
import { getTodosQueryKey } from '@acme/todo/services/todo-service';
import { useUpdateTodo } from '@acme/todo/services/todo-service';
const {fetch: updateTodo} = useUpdateTodo()
function updateTodoItem(todoId, updatedDetails) {
updateTodo({
variables: { id: todoId, ...updatedDetails },
optimisticResponse: {
// Construct the optimistic UI update here...
...updatedDetails,
},
updates: [
{
queryKey: getTodoListQueryKey({ userId }),
updateFn: (existingTodos, updatedTodo) => ({
...existingTodos,
todos: existingTodos.todos.map((todo) =>
todo.id === todoId ? { ...todo, ...updatedTodo } : todo
),
}),
},
],
refetchQueries: [
// Alternatively, refetch the query for automatic updates
getTodosQueryKey({ userId }),
],
});
}
Adding a New TODO Item
import { getTodosQueryKey } from '@acme/todo/services/todo-service';
import { useCreateTodo } from '@acme/todo/services/todo-service';
const { fetch: addTodo } = useCreateTodo();
function addNewTodoItem(todoDetails) {
addTodo({
variables: { ...todoDetails },
optimisticResponse: {
id: 'temp-id', // Temporary ID until backend confirmation
...todoDetails,
},
updates: [
{
queryKey: getTodoListQueryKey(),
updateFn: (existingTodos, newTodo) => ({
...existingTodos,
todos: [...existingTodos.todos, newTodo],
}),
},
],
refetchQueries: [
// Alternatively, refetch the query for automatic updates
getTodosQueryKey({ userId }),
],
});
}
For more information on React Query, see the React Query documentation (opens in a new tab).