Skip to content

Commit 1bc2fa1

Browse files
committed
feat: add React Refresh plugin to TanStack Start configurations and introduce SecretLeakButton component for error testing
1 parent 89cb2f2 commit 1bc2fa1

8 files changed

Lines changed: 67 additions & 35 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useState } from "react";
2+
import { env } from "~/env";
3+
4+
function LeakedSecret() {
5+
// Accessing server-only DATABASE_URL directly in client render tree throws an error
6+
return <p>Server key leaked: {env.DATABASE_URL}</p>;
7+
}
8+
9+
export function SecretLeakButton() {
10+
const [attempted, setAttempted] = useState(false);
11+
12+
if (attempted) {
13+
return <LeakedSecret />;
14+
}
15+
16+
return (
17+
<button type="button" onClick={() => setAttempted(true)}>
18+
Try reading DATABASE_URL on the client
19+
</button>
20+
);
21+
}
Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createFileRoute } from "@tanstack/react-router";
22
import { createServerFn } from "@tanstack/react-start";
3-
import { useState } from "react";
4-
import { env } from "../env";
3+
import { SecretLeakButton } from "~/components/secret-leak-button";
4+
import { env } from "~/env";
55

66
const getDatabaseHost = createServerFn({ method: "GET" }).handler(() => {
77
// Server functions can safely read server secrets without leaking them to the client:
@@ -14,30 +14,15 @@ export const Route = createFileRoute("/")({
1414
loader: () => getDatabaseHost(),
1515
});
1616

17-
function tryReadServerKeyOnClient(): string {
18-
try {
19-
return `Server key leaked: ${env.DATABASE_URL}`;
20-
} catch (error) {
21-
return error instanceof Error ? error.message : String(error);
22-
}
23-
}
24-
2517
function Home() {
2618
const dbHost = Route.useLoaderData();
27-
const [clientReadResult, setClientReadResult] = useState<string | null>(null);
2819

2920
return (
3021
<main style={{ fontFamily: "system-ui, sans-serif", padding: "2rem" }}>
3122
<h1>API: {env.VITE_API_URL}</h1>
3223
<p>Public key inlined into the client bundle: {env.VITE_API_URL}</p>
3324
<p>Database host loaded through createServerFn: {dbHost}</p>
34-
<button
35-
type="button"
36-
onClick={() => setClientReadResult(tryReadServerKeyOnClient())}
37-
>
38-
Try reading DATABASE_URL on the client
39-
</button>
40-
{clientReadResult ? <p>{clientReadResult}</p> : null}
25+
<SecretLeakButton />
4126
</main>
4227
);
4328
}

apps/playgrounds/tanstack-start/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"],
33
"compilerOptions": {
4+
"paths": {
5+
"~/*": ["./src/*"]
6+
},
47
"strict": true,
58
"esModuleInterop": true,
69
"jsx": "react-jsx",

apps/playgrounds/tanstack-start/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import viteReact from "@vitejs/plugin-react";
44
import { defineConfig } from "vite";
55

66
export default defineConfig({
7+
resolve: {
8+
tsconfigPaths: true,
9+
},
710
server: {
811
port: 3000,
912
},

apps/www/content/docs/frameworks/tanstack-start.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ Install `@vitejs/plugin-react` if it is not already a dependency. If you
9595
aren't using ArkType, import the ArkEnv plugin from
9696
`@arkenv/vite-plugin/standard`.
9797

98+
:::warning[React Refresh plugin required for client hydration]
99+
TanStack Start React dev mode requires the React Refresh runtime (`/@react-refresh`). You must include `@vitejs/plugin-react` (or `@vitejs/plugin-react-swc`) in your Vite `plugins` array and place it **after** `tanstackStart()`.
100+
101+
If the React plugin is missing, Vite fails to load the client entry (`/@react-refresh could not be resolved`). The page will still render via SSR, but client-side hydration fails, leaving interactive handlers (like `onClick` listeners) unattached.
102+
:::
103+
98104
### Define your schema
99105

100106
Create an `env.ts` file in your source tree. Keys your client bundle reads must

arkenv.code-workspace

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@
9999
"path": "apps/playgrounds/solid-start",
100100
"name": "🛝 solid-start"
101101
},
102+
{
103+
"path": "apps/playgrounds/tanstack-start",
104+
"name": "🛝 tanstack-start"
105+
},
102106
{
103107
"path": "apps/playgrounds/bun-react",
104108
"name": "🛝 bun-react"
@@ -163,6 +167,10 @@
163167
"path": "examples/with-solid-start",
164168
"name": "💡 with-solid-start"
165169
},
170+
{
171+
"path": "examples/with-tanstack-start",
172+
"name": "💡 with-tanstack-start"
173+
},
166174
{
167175
"path": "examples/with-zod",
168176
"name": "💡 with-zod"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useState } from "react";
2+
import { env } from "../env";
3+
4+
function LeakedSecret() {
5+
// Accessing server-only DATABASE_URL directly in client render tree throws an error
6+
return <p>Server key leaked: {env.DATABASE_URL}</p>;
7+
}
8+
9+
export function SecretLeakButton() {
10+
const [attempted, setAttempted] = useState(false);
11+
12+
if (attempted) {
13+
return <LeakedSecret />;
14+
}
15+
16+
return (
17+
<button type="button" onClick={() => setAttempted(true)}>
18+
Try reading DATABASE_URL on the client
19+
</button>
20+
);
21+
}
Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createFileRoute } from "@tanstack/react-router";
22
import { createServerFn } from "@tanstack/react-start";
3-
import { useState } from "react";
3+
import { SecretLeakButton } from "../components/secret-leak-button";
44
import { env } from "../env";
55

66
const getDatabaseHost = createServerFn({ method: "GET" }).handler(() => {
@@ -14,30 +14,15 @@ export const Route = createFileRoute("/")({
1414
loader: () => getDatabaseHost(),
1515
});
1616

17-
function tryReadServerKeyOnClient(): string {
18-
try {
19-
return `Server key leaked: ${env.DATABASE_URL}`;
20-
} catch (error) {
21-
return error instanceof Error ? error.message : String(error);
22-
}
23-
}
24-
2517
function Home() {
2618
const dbHost = Route.useLoaderData();
27-
const [clientReadResult, setClientReadResult] = useState<string | null>(null);
2819

2920
return (
3021
<main style={{ fontFamily: "system-ui, sans-serif", padding: "2rem" }}>
3122
<h1>API: {env.VITE_API_URL}</h1>
3223
<p>Public key inlined into the client bundle: {env.VITE_API_URL}</p>
3324
<p>Database host loaded through createServerFn: {dbHost}</p>
34-
<button
35-
type="button"
36-
onClick={() => setClientReadResult(tryReadServerKeyOnClient())}
37-
>
38-
Try reading DATABASE_URL on the client
39-
</button>
40-
{clientReadResult ? <p>{clientReadResult}</p> : null}
25+
<SecretLeakButton />
4126
</main>
4227
);
4328
}

0 commit comments

Comments
 (0)