Published on

React Query + Hono API + Next.js

Authors
  • avatar
    Name
    Shelton Ma
    Twitter

1. Setup QueryClientProvider

  1. Install packages

    pnpm add @tanstack/react-query
    
  2. Add provider

    // src/components/query-provider.tsx
    "use client";
    
    // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
    import {
      isServer,
      QueryClient,
      QueryClientProvider,
    } from "@tanstack/react-query";
    
    function makeQueryClient() {
      return new QueryClient({
        defaultOptions: {
          queries: {
            // With SSR, we usually want to set some default staleTime
            // above 0 to avoid refetching immediately on the client
            staleTime: 60 * 1000,
          },
        },
      });
    }
    
    let browserQueryClient: QueryClient | undefined = undefined;
    
    function getQueryClient() {
      if (isServer) {
        // Server: always make a new query client
        return makeQueryClient();
      } else {
        // Browser: make a new query client if we don't already have one
        // This is very important, so we don't re-make a new client if React
        // suspends during the initial render. This may not be needed if we
        // have a suspense boundary BELOW the creation of the query client
        if (!browserQueryClient) browserQueryClient = makeQueryClient();
        return browserQueryClient;
      }
    }
    
    export default function QueryProviders({
      children,
    }: {
      children: React.ReactNode;
    }) {
      // NOTE: Avoid useState when initializing the query client if you don't
      //       have a suspense boundary between this and the code that may
      //       suspend because React will throw away the client on the initial
      //       render if it suspends and there is no boundary
      const queryClient = getQueryClient();
    
      return (
        <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
      );
    }
    
  3. Create providers for more provider

    // /src/components/providers.tsx
    "use client";
    import QueryProviders from "./query-provider";
    
    interface ProvidersProps {
      children: React.ReactNode;
    }
    
    export const Providers = ({ children }: ProvidersProps) => {
      return <QueryProviders>{children}</QueryProviders>;
    };
    
  4. Enable provider in layout

    // src/app/layout.tsx
    import { Providers } from "@/components/providers";
    ...
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <Providers>{children}</Providers>
      </body>
    </html>
    ...
    

2. Use with Hono API

1. useMutation

  1. Create mutation api

    // src/features/conversation/api/use-send-message.tsx
    import { client } from "@/lib/hono";
    import { useMutation } from "@tanstack/react-query";
    import { InferRequestType, InferResponseType } from "hono";
    import { toast } from "sonner";
    
    export type ResponseType = InferResponseType<
      typeof client.api.conversation.$post,
      200
    >;
    
    type RequestType = InferRequestType<
      typeof client.api.conversation.$post
    >["json"];
    
    export const useSendMessage = () => {
      const mutation = useMutation<ResponseType, Error, RequestType>({
        mutationFn: async (json) => {
          const response = await client.api.conversation.$post({ json });
          if (!response.ok) {
            throw new Error("Something went wrong");
          }
          return await response.json();
        },
        onError: () => {
          toast.error("Failed to send message");
        },
      });
      return mutation;
    };
    
  2. useHook in component

    // 
    export default function CustomComponent() {
      ...
      const mutation = useSendMessage();
    
      const onSubmit1 = async (values: z.infer<typeof formSchema>) => {
        // Just mutation, can use mutation.isPending
        // Does not return a Promise, so it can’t be awaited.
        mutation.mutate(
          { message: values["prompt"] },
          {
            onSuccess: (data) => {
              setMessages(data.messages);
              form.reset();
            },
            onError: (data) => {
              console.log(data.message);
            },
          }
        );
      };
      
      const onSubmit2 = async (values: z.infer<typeof formSchema>) => {
        try {
          // mutateAsync, can use `const isLoading = form.formState.isSubmitting;`
          const result = await mutation.mutateAsync({ message: values["prompt"] }) as ApiResponse;
          if (result.messages) {
            const typedMessages = result.messages.map((msg) => ({
              role: msg.role as "user" | "assistant",
              content: msg.content
            }));
            setMessages(typedMessages);
            form.reset();
          }
        } catch (error) {
          console.error("Failed to send message:", error);
        }
      };
      ...
    }
    

2. useQuery

3. useInfiniteQuery

4. useQueryClient

  1. Use useQueryClient to ivalidate queries

    // src/features/projects/api/use-update-project.ts
    import { useMutation, useQueryClient } from "@tanstack/react-query";
    
    export const useUpdateProject = (id: string) => {
      const queryClient = useQueryClient();
    
      const mutation = useMutation<ResponseType, Error, RequestType>({
        mutationKey: ["project", { id }],
        mutationFn: async (json) => {
          const response = ...
          if (!response.ok) {
            throw new Error("Failed to update project");
          }
          return await response.json();
        },
        onSuccess: () => {
          queryClient.invalidateQueries({ queryKey: ["projects"] });
          queryClient.invalidateQueries({ queryKey: ["project", { id }] });
        },
        onError: () => {
          toast.error("Failed to update project");
        },
      });
      return mutation;
    };