|
| 1 | +# TanStack Patterns |
| 2 | + |
| 3 | +## loaderDeps Must Be Specific |
| 4 | + |
| 5 | +Only include properties actually used in the loader. This ensures proper cache invalidation. |
| 6 | + |
| 7 | +```typescript |
| 8 | +// Bad: includes everything |
| 9 | +loaderDeps: ({ search }) => search, |
| 10 | +loader: async ({ deps }) => { |
| 11 | + await fetchData({ page: deps.page, pageSize: deps.pageSize }) |
| 12 | +} |
| 13 | + |
| 14 | +// Good: only what's used |
| 15 | +loaderDeps: ({ search }) => ({ |
| 16 | + page: search.page, |
| 17 | + pageSize: search.pageSize, |
| 18 | +}), |
| 19 | +loader: async ({ deps }) => { |
| 20 | + await fetchData({ page: deps.page, pageSize: deps.pageSize }) |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +## Loaders Are Isomorphic |
| 25 | + |
| 26 | +Loaders run on both server and client. They cannot directly access server-only APIs. |
| 27 | + |
| 28 | +```typescript |
| 29 | +// Bad: direct server API access |
| 30 | +loader: async () => { |
| 31 | + const data = await fs.readFile('data.json') |
| 32 | + return { data } |
| 33 | +} |
| 34 | + |
| 35 | +// Good: call a server function |
| 36 | +loader: async () => { |
| 37 | + const data = await serverFn({ data: { id: '123' } }) |
| 38 | + return { data } |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +## Environment Shaking |
| 43 | + |
| 44 | +TanStack Start strips any code not referenced by a `createServerFn` handler from the client build. |
| 45 | + |
| 46 | +- Server-only code (database, fs) is automatically excluded from client bundles |
| 47 | +- Only code inside `createServerFn` handlers goes to server bundles |
| 48 | +- Code outside handlers is included in both bundles |
| 49 | + |
| 50 | +## Importing Server Functions |
| 51 | + |
| 52 | +Server functions wrapped in `createServerFn` can be imported statically. Never use dynamic imports for server-only code in components. |
| 53 | + |
| 54 | +```typescript |
| 55 | +// Bad: dynamic import causes bundler issues |
| 56 | +const rolesQuery = useQuery({ |
| 57 | + queryFn: async () => { |
| 58 | + const { listRoles } = await import('~/utils/roles.server') |
| 59 | + return listRoles({ data: {} }) |
| 60 | + }, |
| 61 | +}) |
| 62 | + |
| 63 | +// Good: static import |
| 64 | +import { listRoles } from '~/utils/roles.server' |
| 65 | + |
| 66 | +const rolesQuery = useQuery({ |
| 67 | + queryFn: async () => listRoles({ data: {} }), |
| 68 | +}) |
| 69 | +``` |
| 70 | + |
| 71 | +## Server-Only Import Rules |
| 72 | + |
| 73 | +1. `createServerFn` wrappers can be imported statically anywhere |
| 74 | +2. Direct server-only code (database clients, fs) must only be imported: |
| 75 | + - Inside `createServerFn` handlers |
| 76 | + - In `*.server.ts` files |
0 commit comments