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')}
+
+
+
+ )
+}
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 }) => (