Skip to content

Commit

Permalink
feat: trpc
Browse files Browse the repository at this point in the history
  • Loading branch information
tiesen243 committed Dec 11, 2024
1 parent 83a980c commit 978e1e9
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 36 deletions.
29 changes: 29 additions & 0 deletions apps/react-router/app/components/post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { CreatePost } from '@yuki/api/types/post'
import { Button } from '@yuki/ui/button'
import { Input } from '@yuki/ui/input'

import { api } from '@/lib/trpc'

export const Post: React.FC = () => {
const { data: latestPost, isLoading, refetch } = api.post.getLatestPost.useQuery()
const createPost = api.post.createPost.useMutation({ onSuccess: () => refetch() })

return (
<div className="mt-4 w-full max-w-screen-sm">
<span className="text-lg">
Latest post: {isLoading ? 'Loading...' : (latestPost?.content ?? 'No posts')}
</span>
<form
className="flex w-full gap-4"
onSubmit={(e) => {
e.preventDefault()
createPost.mutate(Object.fromEntries(new FormData(e.currentTarget)) as CreatePost)
e.currentTarget.reset()
}}
>
<Input name="content" placeholder="Post's content" disabled={createPost.isPending} />
<Button disabled={createPost.isPending}>Post</Button>
</form>
</div>
)
}
3 changes: 2 additions & 1 deletion apps/react-router/app/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ export const env = createEnv({
*/
runtimeEnv: {
...import.meta.env,
NODE_ENV: process.env.NODE_ENV,

DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,

// VITE_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
Expand Down
21 changes: 21 additions & 0 deletions apps/react-router/app/lib/hooks/use-cookies.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createContext, use } from 'react'

type Cookies<T extends Record<string, string>> = T | undefined

const cookiesContext = createContext<Cookies<Record<string, string>>>(undefined)

export const CookiesProvider: React.FC<{
allCookies: Record<string, string>
children: React.ReactNode
}> = ({ allCookies, children }) => (
<cookiesContext.Provider value={allCookies}>{children}</cookiesContext.Provider>
)

export const useCookies = <T extends Record<string, string>>(): Cookies<T> => {
const context = use(cookiesContext)
if (context === undefined) throw new Error('useCookies must be used within a CookiesProvider')

return {
...context,
} as T
}
64 changes: 64 additions & 0 deletions apps/react-router/app/lib/trpc/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { QueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink, loggerLink } from '@trpc/client'
import { createTRPCReact } from '@trpc/react-query'
import SuperJSON from 'superjson'

import type { AppRouter } from '@yuki/api'

import { useCookies } from '@/lib/hooks/use-cookies'
import { createQueryClient } from '@/lib/trpc/query-client'

let clientQueryClientSingleton: QueryClient | undefined = undefined
const getQueryClient = () => {
if (typeof window === 'undefined') {
// Server: always make a new query client
return createQueryClient()
} else {
// Browser: use singleton pattern to keep the same query client
return (clientQueryClientSingleton ??= createQueryClient())
}
}

export const api = createTRPCReact<AppRouter>()

export const TRPCReactProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
const queryClient = getQueryClient()
const cookies = useCookies<{ auth_session: string }>()

const [trpcClient] = useState(() =>
api.createClient({
links: [
loggerLink({
enabled: (op) =>
// eslint-disable-next-line turbo/no-undeclared-env-vars
import.meta.env.DEV || (op.direction === 'down' && op.result instanceof Error),
}),
httpBatchLink({
transformer: SuperJSON,
url: getBaseUrl() + '/api/trpc',
headers() {
const headers = new Headers()
headers.set('x-trpc-source', 'react-router')
headers.set('Authorization', `Bearer ${cookies?.auth_session}`)
return headers
},
}),
],
}),
)

return (
<QueryClientProvider client={queryClient}>
<api.Provider client={trpcClient} queryClient={queryClient}>
{children}
</api.Provider>
</QueryClientProvider>
)
}

const getBaseUrl = () => {
// if () return `https://your.next.app.url`
return `http://localhost:3000`
}
21 changes: 21 additions & 0 deletions apps/react-router/app/lib/trpc/query-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defaultShouldDehydrateQuery, QueryClient } from '@tanstack/react-query'
import SuperJSON from 'superjson'

export const createQueryClient = () =>
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,
},
dehydrate: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
hydrate: {
deserializeData: SuperJSON.deserialize,
},
},
})
14 changes: 12 additions & 2 deletions apps/react-router/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import stylesheet from '@yuki/ui/tailwind.css?url'
import type { Route } from './+types/root'
import { env } from '@/env'
import { icons, seo } from '@/lib/seo'
import { TRPCReactProvider } from '@/lib/trpc'
import { CookiesProvider } from './lib/hooks/use-cookies'

export const meta = seo({})

Expand Down Expand Up @@ -43,10 +45,18 @@ export const Layout: React.FC<React.PropsWithChildren> = ({ children }) => (
</html>
)

export default () => {
return <Outlet />
export const loader = ({ context }: Route.LoaderArgs) => {
return { cookies: context.cookies }

Check failure on line 49 in apps/react-router/app/root.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an error typed value

Check failure on line 49 in apps/react-router/app/root.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .cookies on an `error` typed value
}

export default ({ loaderData }: Route.ComponentProps) => (
<CookiesProvider allCookies={loaderData.cookies}>

Check failure on line 53 in apps/react-router/app/root.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an error typed value

Check failure on line 53 in apps/react-router/app/root.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .cookies on an `error` typed value
<TRPCReactProvider>
<Outlet />
</TRPCReactProvider>
</CookiesProvider>
)

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = 'Oops!'
let details = 'An unexpected error occurred.'
Expand Down
59 changes: 57 additions & 2 deletions apps/react-router/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,64 @@
import { Button } from '@yuki/ui/button'
import { Typography } from '@yuki/ui/typography'

import type { Route } from './+types/_index'
import { Post } from '@/components/post'

export const loader = ({ context }: Route.LoaderArgs) => {
return { message: context.VALUE_FROM_VERCEL }
return { session: context.cookies.auth_session }

Check failure on line 8 in apps/react-router/app/routes/_index.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an error typed value

Check failure on line 8 in apps/react-router/app/routes/_index.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .cookies on an `error` typed value
}

export default ({ loaderData }: Route.ComponentProps) => {
return <div>{loaderData.message}</div>
const session = loaderData.session

Check failure on line 12 in apps/react-router/app/routes/_index.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe assignment of an error typed value

Check failure on line 12 in apps/react-router/app/routes/_index.tsx

View workflow job for this annotation

GitHub Actions / lint

Unsafe member access .session on an `error` typed value

return (
<main className="container flex min-h-dvh max-w-screen-lg flex-col items-center justify-center overflow-x-hidden">
<div className="pointer-events-none relative flex place-items-center before:absolute before:h-[700px] before:w-[140px] before:translate-x-1 before:translate-y-[-10px] before:rotate-[-32deg] before:rounded-full before:bg-gradient-to-r before:from-[#AB1D1C] before:to-[#E18317] before:opacity-30 before:blur-[100px] before:content-[''] lg:before:h-[700px] lg:before:w-[240px] lg:before:translate-x-[-100px]" />

<img
src="https://tiesen.id.vn/assets/tiesen.png"
width={2500}
height={400}
alt="tiesen"
loading="lazy"
/>

<Typography level="h1" className="mb-4 text-center brightness-150">
A Next.js template with{' '}
<span className="bg-[linear-gradient(135deg,#3178C6,69%,hsl(var(--background)))] bg-clip-text text-transparent">
TypeScript
</span>
,{' '}
<span className="bg-[linear-gradient(135deg,#06B6D4,69%,hsl(var(--background)))] bg-clip-text text-transparent">
Tailwind CSS
</span>
,{' '}
<span className="bg-[linear-gradient(135deg,#4B32C3,69%,hsl(var(--background)))] bg-clip-text text-transparent">
ESLint
</span>{' '}
and{' '}
<span className="bg-[linear-gradient(135deg,#F7B93E,69%,hsl(var(--background)))] bg-clip-text text-transparent">
Prettier
</span>
</Typography>

{session ? (
<>
<div className="flex items-center justify-center gap-4">
<Typography className="text-center text-2xl">Logged in as {session}</Typography>

<form>
<Button size="sm">Sign out</Button>
</form>
</div>

<Post />
</>
) : (
<form action="/api/auth/discord">
<Button size="lg">Sign in with Discord</Button>
</form>
)}
</main>
)
}
4 changes: 4 additions & 0 deletions apps/react-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"@react-router/fs-routes": "^7.0.2",
"@react-router/node": "^7.0.2",
"@t3-oss/env-core": "^0.11.1",
"@tanstack/react-query": "^5.62.7",
"@trpc/client": "^11.0.0-rc.660",
"@trpc/react-query": "^11.0.0-rc.660",
"@vercel/node": "^3.2.29",
"@yuki/api": "workspace:*",
"@yuki/auth": "workspace:*",
Expand All @@ -27,6 +30,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.0.2",
"superjson": "^2.2.2",
"zod": "^3.24.1"
},
"devDependencies": {
Expand Down
38 changes: 25 additions & 13 deletions apps/react-router/server/app.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
import { createRequestHandler } from "@react-router/express";
import express from "express";
import "react-router";
import { createRequestHandler } from '@react-router/express'
import express from 'express'

declare module "react-router" {
import 'react-router'

declare module 'react-router' {
export interface AppLoadContext {
VALUE_FROM_VERCEL: string;
cookies: Record<string, string>
}
}

const app = express();
const app = express()

app.use(
createRequestHandler({
// @ts-expect-error - virtual module provided by React Router at build time
build: () => import("virtual:react-router/server-build"),
getLoadContext() {
build: () => import('virtual:react-router/server-build'),
getLoadContext({ headers }) {
const cookies = headers.cookie
? headers.cookie.split(';').reduce(
(acc, cookie) => {
const [key, value] = cookie.split('=') as [string, string]
acc[key.trim()] = value.trim()
return acc
},
{} as Record<string, string>,
)
: {}

return {
VALUE_FROM_VERCEL: "Hello from Vercel",
};
cookies,
}
},
})
);
}),
)

export default app;
export default app
43 changes: 25 additions & 18 deletions apps/react-router/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { reactRouter } from '@react-router/dev/vite'
import autoprefixer from 'autoprefixer'
import tailwindcss from 'tailwindcss'
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig(({ isSsrBuild, command }) => ({
build: {
rollupOptions: isSsrBuild
? {
input: './server/app.ts',
}
: undefined,
},
css: {
postcss: {
plugins: [tailwindcss, autoprefixer],
export default defineConfig(({ isSsrBuild, command, mode }) => {
const env = loadEnv(mode, process.cwd(), '')

return {
build: {
rollupOptions: isSsrBuild
? {
input: './server/app.ts',
}
: undefined,
},
css: {
postcss: {
plugins: [tailwindcss, autoprefixer],
},
},
ssr: {
noExternal: command === 'build' ? true : undefined,
},
define: {
'process.env': JSON.stringify(env),
},
},
ssr: {
noExternal: command === 'build' ? true : undefined,
},
plugins: [reactRouter(), tsconfigPaths()],
}))
plugins: [reactRouter(), tsconfigPaths()],
}
})
Binary file modified bun.lockb
Binary file not shown.

0 comments on commit 978e1e9

Please sign in to comment.