@odvi/create-dtt-framework 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +16 -13
  3. package/dist/commands/create.js.map +1 -1
  4. package/package.json +3 -2
  5. package/template/.env.example +103 -0
  6. package/template/components.json +22 -0
  7. package/template/docs/framework/01-overview.md +289 -0
  8. package/template/docs/framework/02-techstack.md +503 -0
  9. package/template/docs/framework/api-layer.md +681 -0
  10. package/template/docs/framework/clerk-authentication.md +649 -0
  11. package/template/docs/framework/cli-installation.md +564 -0
  12. package/template/docs/framework/deployment/ci-cd.md +907 -0
  13. package/template/docs/framework/deployment/digitalocean.md +991 -0
  14. package/template/docs/framework/deployment/domain-setup.md +972 -0
  15. package/template/docs/framework/deployment/environment-variables.md +863 -0
  16. package/template/docs/framework/deployment/monitoring.md +927 -0
  17. package/template/docs/framework/deployment/production-checklist.md +649 -0
  18. package/template/docs/framework/deployment/vercel.md +791 -0
  19. package/template/docs/framework/environment-variables.md +658 -0
  20. package/template/docs/framework/health-check-system.md +582 -0
  21. package/template/docs/framework/implementation.md +559 -0
  22. package/template/docs/framework/snowflake-integration.md +591 -0
  23. package/template/docs/framework/state-management.md +615 -0
  24. package/template/docs/framework/supabase-integration.md +581 -0
  25. package/template/docs/framework/testing-guide.md +544 -0
  26. package/template/docs/framework/what-did-i-miss.md +526 -0
  27. package/template/drizzle.config.ts +12 -0
  28. package/template/next.config.js +21 -0
  29. package/template/postcss.config.js +5 -0
  30. package/template/prettier.config.js +4 -0
  31. package/template/public/favicon.ico +0 -0
  32. package/template/src/app/(auth)/layout.tsx +4 -0
  33. package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
  34. package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
  35. package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
  36. package/template/src/app/(dashboard)/health/page.tsx +16 -0
  37. package/template/src/app/(dashboard)/layout.tsx +17 -0
  38. package/template/src/app/api/[[...route]]/route.ts +11 -0
  39. package/template/src/app/api/debug-files/route.ts +33 -0
  40. package/template/src/app/api/webhooks/clerk/route.ts +112 -0
  41. package/template/src/app/layout.tsx +28 -0
  42. package/template/src/app/page.tsx +12 -0
  43. package/template/src/app/providers.tsx +20 -0
  44. package/template/src/components/layouts/navbar.tsx +14 -0
  45. package/template/src/components/shared/loading-spinner.tsx +6 -0
  46. package/template/src/components/ui/badge.tsx +46 -0
  47. package/template/src/components/ui/button.tsx +62 -0
  48. package/template/src/components/ui/card.tsx +92 -0
  49. package/template/src/components/ui/collapsible.tsx +33 -0
  50. package/template/src/components/ui/scroll-area.tsx +58 -0
  51. package/template/src/components/ui/sheet.tsx +139 -0
  52. package/template/src/config/__tests__/env.test.ts +166 -0
  53. package/template/src/config/__tests__/site.test.ts +46 -0
  54. package/template/src/config/env.ts +36 -0
  55. package/template/src/config/site.ts +10 -0
  56. package/template/src/env.js +44 -0
  57. package/template/src/features/__tests__/health-check-config.test.ts +142 -0
  58. package/template/src/features/__tests__/health-check-types.test.ts +201 -0
  59. package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
  60. package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
  61. package/template/src/features/documentation/index.tsx +92 -0
  62. package/template/src/features/documentation/utils/doc-loader.ts +177 -0
  63. package/template/src/features/health-check/components/health-dashboard.tsx +363 -0
  64. package/template/src/features/health-check/config.ts +72 -0
  65. package/template/src/features/health-check/index.ts +4 -0
  66. package/template/src/features/health-check/stores/health-store.ts +14 -0
  67. package/template/src/features/health-check/types.ts +18 -0
  68. package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
  69. package/template/src/hooks/queries/use-health-checks.ts +16 -0
  70. package/template/src/hooks/utils/use-debounce.ts +20 -0
  71. package/template/src/lib/__tests__/utils.test.ts +52 -0
  72. package/template/src/lib/__tests__/validators.test.ts +114 -0
  73. package/template/src/lib/nextbank/client.ts +37 -0
  74. package/template/src/lib/snowflake/client.ts +53 -0
  75. package/template/src/lib/supabase/admin.ts +7 -0
  76. package/template/src/lib/supabase/client.ts +7 -0
  77. package/template/src/lib/supabase/server.ts +23 -0
  78. package/template/src/lib/utils.ts +6 -0
  79. package/template/src/lib/validators.ts +9 -0
  80. package/template/src/middleware.ts +22 -0
  81. package/template/src/server/api/index.ts +22 -0
  82. package/template/src/server/api/middleware/auth.ts +19 -0
  83. package/template/src/server/api/middleware/logger.ts +4 -0
  84. package/template/src/server/api/routes/health/clerk.ts +214 -0
  85. package/template/src/server/api/routes/health/database.ts +117 -0
  86. package/template/src/server/api/routes/health/edge-functions.ts +75 -0
  87. package/template/src/server/api/routes/health/framework.ts +45 -0
  88. package/template/src/server/api/routes/health/index.ts +102 -0
  89. package/template/src/server/api/routes/health/nextbank.ts +67 -0
  90. package/template/src/server/api/routes/health/snowflake.ts +83 -0
  91. package/template/src/server/api/routes/health/storage.ts +163 -0
  92. package/template/src/server/api/routes/users.ts +95 -0
  93. package/template/src/server/db/index.ts +17 -0
  94. package/template/src/server/db/queries/users.ts +8 -0
  95. package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
  96. package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
  97. package/template/src/server/db/schema/health-checks.ts +11 -0
  98. package/template/src/server/db/schema/index.ts +2 -0
  99. package/template/src/server/db/schema/users.ts +16 -0
  100. package/template/src/server/db/schema.ts +26 -0
  101. package/template/src/stores/__tests__/ui-store.test.ts +87 -0
  102. package/template/src/stores/ui-store.ts +14 -0
  103. package/template/src/styles/globals.css +129 -0
  104. package/template/src/test/mocks/clerk.ts +35 -0
  105. package/template/src/test/mocks/snowflake.ts +28 -0
  106. package/template/src/test/mocks/supabase.ts +37 -0
  107. package/template/src/test/setup.ts +69 -0
  108. package/template/src/test/utils/test-helpers.ts +158 -0
  109. package/template/src/types/index.ts +14 -0
  110. package/template/tsconfig.json +43 -0
  111. package/template/vitest.config.ts +44 -0
@@ -0,0 +1,615 @@
1
+ # DTT Framework - State Management
2
+
3
+ ## Overview
4
+
5
+ The DTT Framework uses a dual state management approach:
6
+
7
+ 1. **TanStack Query** for server state (data from APIs, databases)
8
+ 2. **Zustand** for client state (UI state, temporary data)
9
+
10
+ This separation of concerns provides a clear mental model and optimal performance.
11
+
12
+ ### Why This Approach?
13
+
14
+ | State Type | Solution | Why |
15
+ |------------|-----------|------|
16
+ | **Server State** | TanStack Query | Caching, synchronization, background updates, deduplication |
17
+ | **Client State** | Zustand | Simple, lightweight, no boilerplate, no context re-renders |
18
+
19
+ ---
20
+
21
+ ## TanStack Query for Server State
22
+
23
+ ### What is TanStack Query?
24
+
25
+ [TanStack Query](https://tanstack.com/query/latest) (formerly React Query) is a powerful data synchronization library for React. It handles:
26
+
27
+ - **Fetching**: Data fetching with loading states
28
+ - **Caching**: Automatic caching and cache invalidation
29
+ - **Synchronization**: Keeping data fresh with refetching
30
+ - **Deduplication**: Avoiding duplicate requests
31
+ - **Background Updates**: Silent refetching in the background
32
+
33
+ ### Installation
34
+
35
+ ```bash
36
+ pnpm add @tanstack/react-query
37
+ ```
38
+
39
+ ### Setup
40
+
41
+ **Create Query Client**
42
+
43
+ Located at [`src/app/providers.tsx`](../../src/app/providers.tsx):
44
+
45
+ ```typescript
46
+ 'use client'
47
+
48
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
49
+ import { ClerkProvider } from '@clerk/nextjs'
50
+ import { useState } from 'react'
51
+
52
+ export function Providers({ children }: { children: React.ReactNode }) {
53
+ const [queryClient] = useState(() => new QueryClient({
54
+ defaultOptions: {
55
+ queries: {
56
+ staleTime: 60 * 1000, // Data is fresh for 60 seconds
57
+ retry: 1 // Retry failed requests once
58
+ }
59
+ },
60
+ }))
61
+
62
+ return (
63
+ <ClerkProvider>
64
+ <QueryClientProvider client={queryClient}>
65
+ {children}
66
+ </QueryClientProvider>
67
+ </ClerkProvider>
68
+ )
69
+ }
70
+ ```
71
+
72
+ **Wrap App with Providers**
73
+
74
+ ```typescript
75
+ // src/app/layout.tsx
76
+ import { Providers } from './providers'
77
+
78
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
79
+ return (
80
+ <html lang="en">
81
+ <body>
82
+ <Providers>{children}</Providers>
83
+ </body>
84
+ </html>
85
+ )
86
+ }
87
+ ```
88
+
89
+ ### Basic Usage
90
+
91
+ **useQuery Hook**
92
+
93
+ ```typescript
94
+ import { useQuery } from '@tanstack/react-query'
95
+
96
+ function UsersList() {
97
+ const { data, isLoading, error, refetch } = useQuery({
98
+ queryKey: ['users'],
99
+ queryFn: async () => {
100
+ const response = await fetch('/api/users')
101
+ if (!response.ok) throw new Error('Failed to fetch users')
102
+ return response.json()
103
+ },
104
+ })
105
+
106
+ if (isLoading) return <div>Loading...</div>
107
+ if (error) return <div>Error: {error.message}</div>
108
+
109
+ return (
110
+ <div>
111
+ <button onClick={() => refetch()}>Refresh</button>
112
+ {data?.users?.map(user => (
113
+ <div key={user.id}>{user.email}</div>
114
+ ))}
115
+ </div>
116
+ )
117
+ }
118
+ ```
119
+
120
+ **useMutation Hook**
121
+
122
+ ```typescript
123
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
124
+
125
+ function CreateUserForm() {
126
+ const queryClient = useQueryClient()
127
+ const mutation = useMutation({
128
+ mutationFn: async (userData: { name: string; email: string }) => {
129
+ const response = await fetch('/api/users', {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/json' },
132
+ body: JSON.stringify(userData),
133
+ })
134
+ if (!response.ok) throw new Error('Failed to create user')
135
+ return response.json()
136
+ },
137
+ onSuccess: () => {
138
+ // Invalidate and refetch users query
139
+ queryClient.invalidateQueries({ queryKey: ['users'] })
140
+ },
141
+ })
142
+
143
+ const handleSubmit = (e: React.FormEvent) => {
144
+ e.preventDefault()
145
+ const formData = new FormData(e.target as HTMLFormElement)
146
+ mutation.mutate({
147
+ name: formData.get('name') as string,
148
+ email: formData.get('email') as string,
149
+ })
150
+ }
151
+
152
+ return (
153
+ <form onSubmit={handleSubmit}>
154
+ <input name="name" placeholder="Name" />
155
+ <input name="email" placeholder="Email" />
156
+ <button type="submit" disabled={mutation.isPending}>
157
+ {mutation.isPending ? 'Creating...' : 'Create User'}
158
+ </button>
159
+ </form>
160
+ )
161
+ }
162
+ ```
163
+
164
+ ### Query Keys
165
+
166
+ Query keys are used to identify and manage queries:
167
+
168
+ ```typescript
169
+ // Simple key
170
+ useQuery({ queryKey: ['users'], queryFn: fetchUsers })
171
+
172
+ // Key with parameters
173
+ useQuery({
174
+ queryKey: ['user', userId],
175
+ queryFn: () => fetchUser(userId)
176
+ })
177
+
178
+ // Complex key
179
+ useQuery({
180
+ queryKey: ['posts', { page, limit }],
181
+ queryFn: () => fetchPosts(page, limit)
182
+ })
183
+ ```
184
+
185
+ ### Cache Invalidation
186
+
187
+ ```typescript
188
+ const queryClient = useQueryClient()
189
+
190
+ // Invalidate specific query
191
+ queryClient.invalidateQueries({ queryKey: ['users'] })
192
+
193
+ // Invalidate all queries
194
+ queryClient.invalidateQueries()
195
+
196
+ // Invalidate queries matching a predicate
197
+ queryClient.invalidateQueries({
198
+ predicate: (query) => query.queryKey[0] === 'users'
199
+ })
200
+ ```
201
+
202
+ ### Prefetching
203
+
204
+ ```typescript
205
+ const queryClient = useQueryClient()
206
+
207
+ // Prefetch data before it's needed
208
+ useEffect(() => {
209
+ queryClient.prefetchQuery({
210
+ queryKey: ['user', nextUserId],
211
+ queryFn: () => fetchUser(nextUserId),
212
+ })
213
+ }, [nextUserId])
214
+ ```
215
+
216
+ ### Custom Hooks
217
+
218
+ Located at [`src/hooks/queries/use-health-checks.ts`](../../src/hooks/queries/use-health-checks.ts):
219
+
220
+ ```typescript
221
+ 'use client'
222
+
223
+ import { useQuery } from '@tanstack/react-query'
224
+
225
+ export function useHealthChecks() {
226
+ return useQuery({
227
+ queryKey: ['health-checks'],
228
+ queryFn: async () => {
229
+ const response = await fetch('/api/health/all')
230
+ if (!response.ok) throw new Error('Failed to fetch health checks')
231
+ return response.json()
232
+ },
233
+ })
234
+ }
235
+ ```
236
+
237
+ **Usage:**
238
+
239
+ ```typescript
240
+ import { useHealthChecks } from '@/hooks/queries/use-health-checks'
241
+
242
+ function HealthPage() {
243
+ const { data, isLoading, error, refetch } = useHealthChecks()
244
+
245
+ if (isLoading) return <div>Loading...</div>
246
+ if (error) return <div>Error: {error.message}</div>
247
+
248
+ return <div>{/* Render health checks */}</div>
249
+ }
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Zustand for Client State
255
+
256
+ ### What is Zustand?
257
+
258
+ [Zustand](https://zustand-demo.pmnd.rs/) is a small, fast, and scalable state management solution. It provides:
259
+
260
+ - **Simple API**: Minimal boilerplate
261
+ - **No Context**: Avoids unnecessary re-renders
262
+ - **TypeScript Support**: Full type safety
263
+ - **DevTools**: Built-in DevTools integration
264
+ - **Middleware**: Composable middleware
265
+
266
+ ### Installation
267
+
268
+ ```bash
269
+ pnpm add zustand
270
+ ```
271
+
272
+ ### Basic Usage
273
+
274
+ **Create a Store**
275
+
276
+ Located at [`src/stores/ui-store.ts`](../../src/stores/ui-store.ts):
277
+
278
+ ```typescript
279
+ 'use client'
280
+
281
+ import { create } from 'zustand'
282
+
283
+ interface UIState {
284
+ sidebarOpen: boolean
285
+ toggleSidebar: () => void
286
+ setSidebarOpen: (open: boolean) => void
287
+ }
288
+
289
+ export const useUIStore = create<UIState>((set) => ({
290
+ sidebarOpen: false,
291
+ toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
292
+ setSidebarOpen: (open) => set({ sidebarOpen: open }),
293
+ }))
294
+ ```
295
+
296
+ **Use the Store**
297
+
298
+ ```typescript
299
+ import { useUIStore } from '@/stores/ui-store'
300
+
301
+ function Sidebar() {
302
+ const sidebarOpen = useUIStore((state) => state.sidebarOpen)
303
+ const toggleSidebar = useUIStore((state) => state.toggleSidebar)
304
+
305
+ return (
306
+ <div>
307
+ <button onClick={toggleSidebar}>Toggle Sidebar</button>
308
+ {sidebarOpen && <div>Sidebar Content</div>}
309
+ </div>
310
+ )
311
+ }
312
+ ```
313
+
314
+ ### Advanced Patterns
315
+
316
+ **Multiple Selectors**
317
+
318
+ ```typescript
319
+ // Select multiple state values
320
+ const { sidebarOpen, theme } = useUIStore((state) => ({
321
+ sidebarOpen: state.sidebarOpen,
322
+ theme: state.theme,
323
+ }))
324
+ ```
325
+
326
+ **Actions**
327
+
328
+ ```typescript
329
+ interface UIState {
330
+ // State
331
+ sidebarOpen: boolean
332
+ theme: 'light' | 'dark'
333
+
334
+ // Actions
335
+ toggleSidebar: () => void
336
+ setSidebarOpen: (open: boolean) => void
337
+ setTheme: (theme: 'light' | 'dark') => void
338
+ }
339
+
340
+ export const useUIStore = create<UIState>((set) => ({
341
+ sidebarOpen: false,
342
+ theme: 'light',
343
+
344
+ toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
345
+ setSidebarOpen: (open) => set({ sidebarOpen: open }),
346
+ setTheme: (theme) => set({ theme }),
347
+ }))
348
+ ```
349
+
350
+ **Async Actions**
351
+
352
+ ```typescript
353
+ interface DataState {
354
+ data: any[]
355
+ loading: boolean
356
+ error: string | null
357
+ fetchData: () => Promise<void>
358
+ }
359
+
360
+ export const useDataStore = create<DataState>((set) => ({
361
+ data: [],
362
+ loading: false,
363
+ error: null,
364
+
365
+ fetchData: async () => {
366
+ set({ loading: true, error: null })
367
+ try {
368
+ const response = await fetch('/api/data')
369
+ const data = await response.json()
370
+ set({ data, loading: false })
371
+ } catch (error) {
372
+ set({ error: error.message, loading: false })
373
+ }
374
+ },
375
+ }))
376
+ ```
377
+
378
+ **Middleware**
379
+
380
+ ```typescript
381
+ import { devtools, persist } from 'zustand/middleware'
382
+
383
+ export const useUIStore = create<UIState>()(
384
+ devtools(
385
+ persist(
386
+ (set) => ({
387
+ sidebarOpen: false,
388
+ toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
389
+ }),
390
+ { name: 'ui-storage' }
391
+ )
392
+ )
393
+ )
394
+ ```
395
+
396
+ ### Slices
397
+
398
+ For larger stores, you can use slices:
399
+
400
+ ```typescript
401
+ // slices/sidebarSlice.ts
402
+ import { create } from 'zustand'
403
+
404
+ export const createSidebarSlice = (set: any) => ({
405
+ sidebarOpen: false,
406
+ toggleSidebar: () => set((state: any) => ({ sidebarOpen: !state.sidebarOpen })),
407
+ })
408
+
409
+ // slices/themeSlice.ts
410
+ export const createThemeSlice = (set: any) => ({
411
+ theme: 'light' as 'light' | 'dark',
412
+ setTheme: (theme: 'light' | 'dark') => set({ theme }),
413
+ })
414
+
415
+ // index.ts
416
+ import { create } from 'zustand'
417
+ import { createSidebarSlice } from './slices/sidebarSlice'
418
+ import { createThemeSlice } from './slices/themeSlice'
419
+
420
+ export const useUIStore = create<UIState>((set) => ({
421
+ ...createSidebarSlice(set),
422
+ ...createThemeSlice(set),
423
+ }))
424
+ ```
425
+
426
+ ---
427
+
428
+ ## Usage Patterns and Examples
429
+
430
+ ### Pattern 1: Fetch and Display Data
431
+
432
+ ```typescript
433
+ import { useQuery } from '@tanstack/react-query'
434
+
435
+ function UserProfile({ userId }: { userId: string }) {
436
+ const { data, isLoading, error } = useQuery({
437
+ queryKey: ['user', userId],
438
+ queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
439
+ })
440
+
441
+ if (isLoading) return <div>Loading...</div>
442
+ if (error) return <div>Error loading user</div>
443
+
444
+ return (
445
+ <div>
446
+ <h1>{data.user.name}</h1>
447
+ <p>{data.user.email}</p>
448
+ </div>
449
+ )
450
+ }
451
+ ```
452
+
453
+ ### Pattern 2: Create and Invalidate
454
+
455
+ ```typescript
456
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
457
+
458
+ function CreateUserForm() {
459
+ const queryClient = useQueryClient()
460
+
461
+ const mutation = useMutation({
462
+ mutationFn: (userData: any) =>
463
+ fetch('/api/users', {
464
+ method: 'POST',
465
+ headers: { 'Content-Type': 'application/json' },
466
+ body: JSON.stringify(userData),
467
+ }).then(r => r.json()),
468
+ onSuccess: () => {
469
+ queryClient.invalidateQueries({ queryKey: ['users'] })
470
+ },
471
+ })
472
+
473
+ const handleSubmit = (e: React.FormEvent) => {
474
+ e.preventDefault()
475
+ const formData = new FormData(e.target as HTMLFormElement)
476
+ mutation.mutate({
477
+ name: formData.get('name'),
478
+ email: formData.get('email'),
479
+ })
480
+ }
481
+
482
+ return (
483
+ <form onSubmit={handleSubmit}>
484
+ <input name="name" required />
485
+ <input name="email" required />
486
+ <button type="submit" disabled={mutation.isPending}>
487
+ {mutation.isPending ? 'Creating...' : 'Create User'}
488
+ </button>
489
+ </form>
490
+ )
491
+ }
492
+ ```
493
+
494
+ ### Pattern 3: Optimistic Updates
495
+
496
+ ```typescript
497
+ const mutation = useMutation({
498
+ mutationFn: updateTodo,
499
+ onMutate: async (newTodo) => {
500
+ // Cancel outgoing refetches
501
+ await queryClient.cancelQueries({ queryKey: ['todos'] })
502
+
503
+ // Snapshot previous value
504
+ const previousTodos = queryClient.getQueryData(['todos'])
505
+
506
+ // Optimistically update
507
+ queryClient.setQueryData(['todos'], (old: any) =>
508
+ old?.map((todo: any) =>
509
+ todo.id === newTodo.id ? { ...todo, ...newTodo } : todo
510
+ )
511
+ )
512
+
513
+ return { previousTodos }
514
+ },
515
+ onError: (err, newTodo, context) => {
516
+ // Rollback on error
517
+ queryClient.setQueryData(['todos'], context?.previousTodos)
518
+ },
519
+ onSettled: () => {
520
+ // Always refetch after error or success
521
+ queryClient.invalidateQueries({ queryKey: ['todos'] })
522
+ },
523
+ })
524
+ ```
525
+
526
+ ### Pattern 4: UI State with Zustand
527
+
528
+ ```typescript
529
+ import { useUIStore } from '@/stores/ui-store'
530
+
531
+ function Header() {
532
+ const { sidebarOpen, toggleSidebar, theme, setTheme } = useUIStore()
533
+
534
+ return (
535
+ <header>
536
+ <button onClick={toggleSidebar}>
537
+ {sidebarOpen ? 'Close' : 'Open'} Sidebar
538
+ </button>
539
+ <select
540
+ value={theme}
541
+ onChange={(e) => setTheme(e.target.value as 'light' | 'dark')}
542
+ >
543
+ <option value="light">Light</option>
544
+ <option value="dark">Dark</option>
545
+ </select>
546
+ </header>
547
+ )
548
+ }
549
+ ```
550
+
551
+ ### Pattern 5: Combining Both
552
+
553
+ ```typescript
554
+ function TodoApp() {
555
+ // Server state from API
556
+ const { data: todos, isLoading } = useQuery({
557
+ queryKey: ['todos'],
558
+ queryFn: fetchTodos,
559
+ })
560
+
561
+ // Client state for UI
562
+ const { filter, setFilter } = useTodoStore()
563
+
564
+ // Filter todos based on UI state
565
+ const filteredTodos = todos?.filter(todo => {
566
+ if (filter === 'active') return !todo.completed
567
+ if (filter === 'completed') return todo.completed
568
+ return true
569
+ })
570
+
571
+ return (
572
+ <div>
573
+ <div>
574
+ <button onClick={() => setFilter('all')}>All</button>
575
+ <button onClick={() => setFilter('active')}>Active</button>
576
+ <button onClick={() => setFilter('completed')}>Completed</button>
577
+ </div>
578
+ {isLoading ? <div>Loading...</div> : (
579
+ <ul>
580
+ {filteredTodos?.map(todo => (
581
+ <li key={todo.id}>{todo.title}</li>
582
+ ))}
583
+ </ul>
584
+ )}
585
+ </div>
586
+ )
587
+ }
588
+ ```
589
+
590
+ ---
591
+
592
+ ## Best Practices
593
+
594
+ ### TanStack Query
595
+
596
+ 1. **Use meaningful query keys**: Structure keys to reflect data hierarchy
597
+ 2. **Set appropriate stale time**: Balance freshness and performance
598
+ 3. **Use mutations for writes**: Separate reads from writes
599
+ 4. **Invalidate after mutations**: Keep cache in sync
600
+ 5. **Use custom hooks**: Reuse query logic across components
601
+
602
+ ### Zustand
603
+
604
+ 1. **Keep stores focused**: One store per domain
605
+ 2. **Use selectors**: Subscribe only to needed state
606
+ 3. **Avoid putting server state in Zustand**: Use TanStack Query instead
607
+ 4. **Use middleware when needed**: DevTools, persistence, etc.
608
+
609
+ ---
610
+
611
+ ## Related Documentation
612
+
613
+ - [API Layer](./api-layer.md) - API endpoints for data fetching
614
+ - [Health Check System](./health-check-system.md) - Health check data fetching
615
+ - [Clerk Authentication](./clerk-authentication.md) - Authentication state