Skip to content

Commit

Permalink
refactor: use tRPC queryOptions API (#1305)
Browse files Browse the repository at this point in the history
* queryOptions API

* cool
  • Loading branch information
juliusmarminge authored Feb 18, 2025
1 parent 8589f6f commit 4690233
Show file tree
Hide file tree
Showing 15 changed files with 251 additions and 194 deletions.
2 changes: 1 addition & 1 deletion apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"@shopify/flash-list": "1.7.2",
"@tanstack/react-query": "catalog:",
"@trpc/client": "catalog:",
"@trpc/react-query": "catalog:",
"@trpc/server": "catalog:",
"@trpc/tanstack-react-query": "catalog:",
"expo": "~52.0.27",
"expo-constants": "~17.0.4",
"expo-dev-client": "~5.0.10",
Expand Down
8 changes: 5 additions & 3 deletions apps/expo/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { useColorScheme } from "nativewind";

import { TRPCProvider } from "~/utils/api";
import { queryClient } from "~/utils/api";

import "../styles.css";

import { QueryClientProvider } from "@tanstack/react-query";

// This is the main layout of the app
// It wraps your pages with the providers they need
export default function RootLayout() {
const { colorScheme } = useColorScheme();
return (
<TRPCProvider>
<QueryClientProvider client={queryClient}>
{/*
The Stack component displays the current page.
It also allows you to configure your screens
Expand All @@ -29,6 +31,6 @@ export default function RootLayout() {
}}
/>
<StatusBar />
</TRPCProvider>
</QueryClientProvider>
);
}
36 changes: 21 additions & 15 deletions apps/expo/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState } from "react";
import React, { useState } from "react";
import { Button, Pressable, Text, TextInput, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Link, Stack } from "expo-router";
import { FlashList } from "@shopify/flash-list";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import type { RouterOutputs } from "~/utils/api";
import { api } from "~/utils/api";
import { trpc } from "~/utils/api";
import { useSignIn, useSignOut, useUser } from "~/utils/auth";

function PostCard(props: {
Expand Down Expand Up @@ -38,18 +39,20 @@ function PostCard(props: {
}

function CreatePost() {
const utils = api.useUtils();
const queryClient = useQueryClient();

const [title, setTitle] = useState("");
const [content, setContent] = useState("");

const { mutate, error } = api.post.create.useMutation({
async onSuccess() {
setTitle("");
setContent("");
await utils.post.all.invalidate();
},
});
const { mutate, error } = useMutation(
trpc.post.create.mutationOptions({
async onSuccess() {
setTitle("");
setContent("");
await queryClient.invalidateQueries(trpc.post.all.queryFilter());
},
}),
);

return (
<View className="mt-4 flex gap-2">
Expand Down Expand Up @@ -115,13 +118,16 @@ function MobileAuth() {
}

export default function Index() {
const utils = api.useUtils();
const queryClient = useQueryClient();

const postQuery = api.post.all.useQuery();
const postQuery = useQuery(trpc.post.all.queryOptions());

const deletePostMutation = api.post.delete.useMutation({
onSettled: () => utils.post.all.invalidate(),
});
const deletePostMutation = useMutation(
trpc.post.delete.mutationOptions({
onSettled: () =>
queryClient.invalidateQueries(trpc.post.all.queryFilter()),
}),
);

return (
<SafeAreaView className="bg-background">
Expand Down
5 changes: 3 additions & 2 deletions apps/expo/src/app/post/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { SafeAreaView, Text, View } from "react-native";
import { Stack, useGlobalSearchParams } from "expo-router";
import { useQuery } from "@tanstack/react-query";

import { api } from "~/utils/api";
import { trpc } from "~/utils/api";

export default function Post() {
const { id } = useGlobalSearchParams();
if (!id || typeof id !== "string") throw new Error("unreachable");
const { data } = api.post.byId.useQuery({ id });
const { data } = useQuery(trpc.post.byId.queryOptions({ id }));

if (!data) return null;

Expand Down
80 changes: 36 additions & 44 deletions apps/expo/src/utils/api.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,49 @@
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import { QueryClient } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query";
import superjson from "superjson";

import type { AppRouter } from "@acme/api";

import { getBaseUrl } from "./base-url";
import { getToken } from "./session-store";

/**
* A set of typesafe hooks for consuming your API.
*/
export const api = createTRPCReact<AppRouter>();
export { type RouterInputs, type RouterOutputs } from "@acme/api";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ...
},
},
});

/**
* A wrapper for your app that provides the TRPC context.
* Use only in _app.tsx
* A set of typesafe hooks for consuming your API.
*/
export function TRPCProvider(props: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
api.createClient({
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
colorMode: "ansi",
}),
httpBatchLink({
transformer: superjson,
url: `${getBaseUrl()}/api/trpc`,
headers() {
const headers = new Map<string, string>();
headers.set("x-trpc-source", "expo-react");
export const trpc = createTRPCOptionsProxy<AppRouter>({
client: createTRPCClient({
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
colorMode: "ansi",
}),
httpBatchLink({
transformer: superjson,
url: `${getBaseUrl()}/api/trpc`,
headers() {
const headers = new Map<string, string>();
headers.set("x-trpc-source", "expo-react");

const token = getToken();
if (token) headers.set("Authorization", `Bearer ${token}`);
const token = getToken();
if (token) headers.set("Authorization", `Bearer ${token}`);

return Object.fromEntries(headers);
},
}),
],
}),
);
return Object.fromEntries(headers);
},
}),
],
}),
queryClient,
});

return (
<api.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</api.Provider>
);
}
export { type RouterInputs, type RouterOutputs } from "@acme/api";
15 changes: 8 additions & 7 deletions apps/expo/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as Linking from "expo-linking";
import { useRouter } from "expo-router";
import * as Browser from "expo-web-browser";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import { api } from "./api";
import { trpc } from "./api";
import { getBaseUrl } from "./base-url";
import { deleteToken, setToken } from "./session-store";

Expand All @@ -25,33 +26,33 @@ export const signIn = async () => {
};

export const useUser = () => {
const { data: session } = api.auth.getSession.useQuery();
const { data: session } = useQuery(trpc.auth.getSession.queryOptions());
return session?.user ?? null;
};

export const useSignIn = () => {
const utils = api.useUtils();
const queryClient = useQueryClient();
const router = useRouter();

return async () => {
const success = await signIn();
if (!success) return;

await utils.invalidate();
await queryClient.invalidateQueries(trpc.queryFilter());
router.replace("/");
};
};

export const useSignOut = () => {
const utils = api.useUtils();
const signOut = api.auth.signOut.useMutation();
const queryClient = useQueryClient();
const signOut = useMutation(trpc.auth.signOut.mutationOptions());
const router = useRouter();

return async () => {
const res = await signOut.mutateAsync();
if (!res.success) return;
await deleteToken();
await utils.invalidate();
await queryClient.invalidateQueries(trpc.queryFilter());
router.replace("/");
};
};
2 changes: 1 addition & 1 deletion apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"@t3-oss/env-nextjs": "^0.12.0",
"@tanstack/react-query": "catalog:",
"@trpc/client": "catalog:",
"@trpc/react-query": "catalog:",
"@trpc/server": "catalog:",
"@trpc/tanstack-react-query": "catalog:",
"geist": "^1.3.1",
"next": "^14.2.23",
"react": "catalog:react18",
Expand Down
71 changes: 42 additions & 29 deletions apps/nextjs/src/app/_components/posts.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"use client";

import {
useMutation,
useQueryClient,
useSuspenseQuery,
} from "@tanstack/react-query";

import type { RouterOutputs } from "@acme/api";
import { CreatePostSchema } from "@acme/db/schema";
import { cn } from "@acme/ui";
Expand All @@ -15,9 +21,10 @@ import {
import { Input } from "@acme/ui/input";
import { toast } from "@acme/ui/toast";

import { api } from "~/trpc/react";
import { useTRPC } from "~/trpc/react";

export function CreatePostForm() {
const trpc = useTRPC();
const form = useForm({
schema: CreatePostSchema,
defaultValues: {
Expand All @@ -26,20 +33,22 @@ export function CreatePostForm() {
},
});

const utils = api.useUtils();
const createPost = api.post.create.useMutation({
onSuccess: async () => {
form.reset();
await utils.post.invalidate();
},
onError: (err) => {
toast.error(
err.data?.code === "UNAUTHORIZED"
? "You must be logged in to post"
: "Failed to create post",
);
},
});
const queryClient = useQueryClient();
const createPost = useMutation(
trpc.post.create.mutationOptions({
onSuccess: async () => {
form.reset();
await queryClient.invalidateQueries(trpc.post.queryFilter());
},
onError: (err) => {
toast.error(
err.data?.code === "UNAUTHORIZED"
? "You must be logged in to post"
: "Failed to create post",
);
},
}),
);

return (
<Form {...form}>
Expand Down Expand Up @@ -80,7 +89,8 @@ export function CreatePostForm() {
}

export function PostList() {
const [posts] = api.post.all.useSuspenseQuery();
const trpc = useTRPC();
const { data: posts } = useSuspenseQuery(trpc.post.all.queryOptions());

if (posts.length === 0) {
return (
Expand Down Expand Up @@ -108,19 +118,22 @@ export function PostList() {
export function PostCard(props: {
post: RouterOutputs["post"]["all"][number];
}) {
const utils = api.useUtils();
const deletePost = api.post.delete.useMutation({
onSuccess: async () => {
await utils.post.invalidate();
},
onError: (err) => {
toast.error(
err.data?.code === "UNAUTHORIZED"
? "You must be logged in to delete a post"
: "Failed to delete post",
);
},
});
const trpc = useTRPC();
const queryClient = useQueryClient();
const deletePost = useMutation(
trpc.post.delete.mutationOptions({
onSuccess: async () => {
await queryClient.invalidateQueries(trpc.post.queryFilter());
},
onError: (err) => {
toast.error(
err.data?.code === "UNAUTHORIZED"
? "You must be logged in to delete a post"
: "Failed to delete post",
);
},
}),
);

return (
<div className="flex flex-row rounded-lg bg-muted p-4">
Expand Down
Loading

0 comments on commit 4690233

Please sign in to comment.