Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use tRPC queryOptions API #1305

Merged
merged 2 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading