@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.
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +16 -13
- package/dist/commands/create.js.map +1 -1
- package/package.json +3 -2
- package/template/.env.example +103 -0
- package/template/components.json +22 -0
- package/template/docs/framework/01-overview.md +289 -0
- package/template/docs/framework/02-techstack.md +503 -0
- package/template/docs/framework/api-layer.md +681 -0
- package/template/docs/framework/clerk-authentication.md +649 -0
- package/template/docs/framework/cli-installation.md +564 -0
- package/template/docs/framework/deployment/ci-cd.md +907 -0
- package/template/docs/framework/deployment/digitalocean.md +991 -0
- package/template/docs/framework/deployment/domain-setup.md +972 -0
- package/template/docs/framework/deployment/environment-variables.md +863 -0
- package/template/docs/framework/deployment/monitoring.md +927 -0
- package/template/docs/framework/deployment/production-checklist.md +649 -0
- package/template/docs/framework/deployment/vercel.md +791 -0
- package/template/docs/framework/environment-variables.md +658 -0
- package/template/docs/framework/health-check-system.md +582 -0
- package/template/docs/framework/implementation.md +559 -0
- package/template/docs/framework/snowflake-integration.md +591 -0
- package/template/docs/framework/state-management.md +615 -0
- package/template/docs/framework/supabase-integration.md +581 -0
- package/template/docs/framework/testing-guide.md +544 -0
- package/template/docs/framework/what-did-i-miss.md +526 -0
- package/template/drizzle.config.ts +12 -0
- package/template/next.config.js +21 -0
- package/template/postcss.config.js +5 -0
- package/template/prettier.config.js +4 -0
- package/template/public/favicon.ico +0 -0
- package/template/src/app/(auth)/layout.tsx +4 -0
- package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
- package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
- package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
- package/template/src/app/(dashboard)/health/page.tsx +16 -0
- package/template/src/app/(dashboard)/layout.tsx +17 -0
- package/template/src/app/api/[[...route]]/route.ts +11 -0
- package/template/src/app/api/debug-files/route.ts +33 -0
- package/template/src/app/api/webhooks/clerk/route.ts +112 -0
- package/template/src/app/layout.tsx +28 -0
- package/template/src/app/page.tsx +12 -0
- package/template/src/app/providers.tsx +20 -0
- package/template/src/components/layouts/navbar.tsx +14 -0
- package/template/src/components/shared/loading-spinner.tsx +6 -0
- package/template/src/components/ui/badge.tsx +46 -0
- package/template/src/components/ui/button.tsx +62 -0
- package/template/src/components/ui/card.tsx +92 -0
- package/template/src/components/ui/collapsible.tsx +33 -0
- package/template/src/components/ui/scroll-area.tsx +58 -0
- package/template/src/components/ui/sheet.tsx +139 -0
- package/template/src/config/__tests__/env.test.ts +166 -0
- package/template/src/config/__tests__/site.test.ts +46 -0
- package/template/src/config/env.ts +36 -0
- package/template/src/config/site.ts +10 -0
- package/template/src/env.js +44 -0
- package/template/src/features/__tests__/health-check-config.test.ts +142 -0
- package/template/src/features/__tests__/health-check-types.test.ts +201 -0
- package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
- package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
- package/template/src/features/documentation/index.tsx +92 -0
- package/template/src/features/documentation/utils/doc-loader.ts +177 -0
- package/template/src/features/health-check/components/health-dashboard.tsx +363 -0
- package/template/src/features/health-check/config.ts +72 -0
- package/template/src/features/health-check/index.ts +4 -0
- package/template/src/features/health-check/stores/health-store.ts +14 -0
- package/template/src/features/health-check/types.ts +18 -0
- package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
- package/template/src/hooks/queries/use-health-checks.ts +16 -0
- package/template/src/hooks/utils/use-debounce.ts +20 -0
- package/template/src/lib/__tests__/utils.test.ts +52 -0
- package/template/src/lib/__tests__/validators.test.ts +114 -0
- package/template/src/lib/nextbank/client.ts +37 -0
- package/template/src/lib/snowflake/client.ts +53 -0
- package/template/src/lib/supabase/admin.ts +7 -0
- package/template/src/lib/supabase/client.ts +7 -0
- package/template/src/lib/supabase/server.ts +23 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/lib/validators.ts +9 -0
- package/template/src/middleware.ts +22 -0
- package/template/src/server/api/index.ts +22 -0
- package/template/src/server/api/middleware/auth.ts +19 -0
- package/template/src/server/api/middleware/logger.ts +4 -0
- package/template/src/server/api/routes/health/clerk.ts +214 -0
- package/template/src/server/api/routes/health/database.ts +117 -0
- package/template/src/server/api/routes/health/edge-functions.ts +75 -0
- package/template/src/server/api/routes/health/framework.ts +45 -0
- package/template/src/server/api/routes/health/index.ts +102 -0
- package/template/src/server/api/routes/health/nextbank.ts +67 -0
- package/template/src/server/api/routes/health/snowflake.ts +83 -0
- package/template/src/server/api/routes/health/storage.ts +163 -0
- package/template/src/server/api/routes/users.ts +95 -0
- package/template/src/server/db/index.ts +17 -0
- package/template/src/server/db/queries/users.ts +8 -0
- package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
- package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
- package/template/src/server/db/schema/health-checks.ts +11 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/users.ts +16 -0
- package/template/src/server/db/schema.ts +26 -0
- package/template/src/stores/__tests__/ui-store.test.ts +87 -0
- package/template/src/stores/ui-store.ts +14 -0
- package/template/src/styles/globals.css +129 -0
- package/template/src/test/mocks/clerk.ts +35 -0
- package/template/src/test/mocks/snowflake.ts +28 -0
- package/template/src/test/mocks/supabase.ts +37 -0
- package/template/src/test/setup.ts +69 -0
- package/template/src/test/utils/test-helpers.ts +158 -0
- package/template/src/types/index.ts +14 -0
- package/template/tsconfig.json +43 -0
- 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
|