diff --git a/apps/react-router/app/components/post.tsx b/apps/react-router/app/components/post.tsx new file mode 100644 index 0000000..3876ccf --- /dev/null +++ b/apps/react-router/app/components/post.tsx @@ -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 ( +
+ + Latest post: {isLoading ? 'Loading...' : (latestPost?.content ?? 'No posts')} + +
{ + e.preventDefault() + createPost.mutate(Object.fromEntries(new FormData(e.currentTarget)) as CreatePost) + e.currentTarget.reset() + }} + > + + +
+
+ ) +} diff --git a/apps/react-router/app/env.ts b/apps/react-router/app/env.ts index 95277b2..4dd0354 100644 --- a/apps/react-router/app/env.ts +++ b/apps/react-router/app/env.ts @@ -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, }, diff --git a/apps/react-router/app/lib/hooks/use-cookies.tsx b/apps/react-router/app/lib/hooks/use-cookies.tsx new file mode 100644 index 0000000..9812141 --- /dev/null +++ b/apps/react-router/app/lib/hooks/use-cookies.tsx @@ -0,0 +1,21 @@ +import { createContext, use } from 'react' + +type Cookies> = T | undefined + +const cookiesContext = createContext>>(undefined) + +export const CookiesProvider: React.FC<{ + allCookies: Record + children: React.ReactNode +}> = ({ allCookies, children }) => ( + {children} +) + +export const useCookies = >(): Cookies => { + const context = use(cookiesContext) + if (context === undefined) throw new Error('useCookies must be used within a CookiesProvider') + + return { + ...context, + } as T +} diff --git a/apps/react-router/app/lib/trpc/index.tsx b/apps/react-router/app/lib/trpc/index.tsx new file mode 100644 index 0000000..3417c28 --- /dev/null +++ b/apps/react-router/app/lib/trpc/index.tsx @@ -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() + +export const TRPCReactProvider: React.FC = ({ 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 ( + + + {children} + + + ) +} + +const getBaseUrl = () => { + // if () return `https://your.next.app.url` + return `http://localhost:3000` +} diff --git a/apps/react-router/app/lib/trpc/query-client.ts b/apps/react-router/app/lib/trpc/query-client.ts new file mode 100644 index 0000000..34494a3 --- /dev/null +++ b/apps/react-router/app/lib/trpc/query-client.ts @@ -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, + }, + }, + }) diff --git a/apps/react-router/app/root.tsx b/apps/react-router/app/root.tsx index 0462e8a..880dab2 100644 --- a/apps/react-router/app/root.tsx +++ b/apps/react-router/app/root.tsx @@ -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({}) @@ -43,10 +45,18 @@ export const Layout: React.FC = ({ children }) => ( ) -export default () => { - return +export const loader = ({ context }: Route.LoaderArgs) => { + return { cookies: context.cookies } } +export default ({ loaderData }: Route.ComponentProps) => ( + + + + + +) + export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { let message = 'Oops!' let details = 'An unexpected error occurred.' diff --git a/apps/react-router/app/routes/_index.tsx b/apps/react-router/app/routes/_index.tsx index fea3091..7e5a01a 100644 --- a/apps/react-router/app/routes/_index.tsx +++ b/apps/react-router/app/routes/_index.tsx @@ -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 } } export default ({ loaderData }: Route.ComponentProps) => { - return
{loaderData.message}
+ const session = loaderData.session + + return ( +
+
+ + tiesen + + + A Next.js template with{' '} + + TypeScript + + ,{' '} + + Tailwind CSS + + ,{' '} + + ESLint + {' '} + and{' '} + + Prettier + + + + {session ? ( + <> +
+ Logged in as {session} + +
+ +
+
+ + + + ) : ( +
+ +
+ )} +
+ ) } diff --git a/apps/react-router/package.json b/apps/react-router/package.json index a23c4d1..182dfb3 100644 --- a/apps/react-router/package.json +++ b/apps/react-router/package.json @@ -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:*", @@ -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": { diff --git a/apps/react-router/server/app.ts b/apps/react-router/server/app.ts index dfadc4f..0a0da85 100644 --- a/apps/react-router/server/app.ts +++ b/apps/react-router/server/app.ts @@ -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 } } -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, + ) + : {} + return { - VALUE_FROM_VERCEL: "Hello from Vercel", - }; + cookies, + } }, - }) -); + }), +) -export default app; +export default app diff --git a/apps/react-router/vite.config.ts b/apps/react-router/vite.config.ts index 7d3679a..64ebf4c 100644 --- a/apps/react-router/vite.config.ts +++ b/apps/react-router/vite.config.ts @@ -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()], + } +}) diff --git a/bun.lockb b/bun.lockb index 0e76100..c65a18f 100755 Binary files a/bun.lockb and b/bun.lockb differ