Gene documentation
Services
React Query Services

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).