@girardmedia/bootspring 1.2.0 → 2.0.3
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/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
# React Context Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for using React Context effectively.
|
|
4
|
-
|
|
5
|
-
## Basic Context
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
// contexts/ThemeContext.tsx
|
|
9
|
-
'use client'
|
|
10
|
-
|
|
11
|
-
import { createContext, useContext, useState, ReactNode } from 'react'
|
|
12
|
-
|
|
13
|
-
type Theme = 'light' | 'dark'
|
|
14
|
-
|
|
15
|
-
interface ThemeContextType {
|
|
16
|
-
theme: Theme
|
|
17
|
-
setTheme: (theme: Theme) => void
|
|
18
|
-
toggleTheme: () => void
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
|
22
|
-
|
|
23
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
24
|
-
const [theme, setTheme] = useState<Theme>('light')
|
|
25
|
-
|
|
26
|
-
const toggleTheme = () => {
|
|
27
|
-
setTheme(prev => prev === 'light' ? 'dark' : 'light')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
|
32
|
-
{children}
|
|
33
|
-
</ThemeContext.Provider>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function useTheme() {
|
|
38
|
-
const context = useContext(ThemeContext)
|
|
39
|
-
|
|
40
|
-
if (context === undefined) {
|
|
41
|
-
throw new Error('useTheme must be used within a ThemeProvider')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return context
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Context with Reducer
|
|
49
|
-
|
|
50
|
-
```tsx
|
|
51
|
-
// contexts/CartContext.tsx
|
|
52
|
-
'use client'
|
|
53
|
-
|
|
54
|
-
import { createContext, useContext, useReducer, ReactNode, Dispatch } from 'react'
|
|
55
|
-
|
|
56
|
-
interface CartItem {
|
|
57
|
-
id: string
|
|
58
|
-
name: string
|
|
59
|
-
price: number
|
|
60
|
-
quantity: number
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface CartState {
|
|
64
|
-
items: CartItem[]
|
|
65
|
-
total: number
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type CartAction =
|
|
69
|
-
| { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
|
|
70
|
-
| { type: 'REMOVE_ITEM'; payload: string }
|
|
71
|
-
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
|
|
72
|
-
| { type: 'CLEAR_CART' }
|
|
73
|
-
|
|
74
|
-
function cartReducer(state: CartState, action: CartAction): CartState {
|
|
75
|
-
switch (action.type) {
|
|
76
|
-
case 'ADD_ITEM': {
|
|
77
|
-
const existing = state.items.find(i => i.id === action.payload.id)
|
|
78
|
-
|
|
79
|
-
if (existing) {
|
|
80
|
-
const items = state.items.map(i =>
|
|
81
|
-
i.id === action.payload.id
|
|
82
|
-
? { ...i, quantity: i.quantity + 1 }
|
|
83
|
-
: i
|
|
84
|
-
)
|
|
85
|
-
return { ...state, items, total: calculateTotal(items) }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const items = [...state.items, { ...action.payload, quantity: 1 }]
|
|
89
|
-
return { ...state, items, total: calculateTotal(items) }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
case 'REMOVE_ITEM': {
|
|
93
|
-
const items = state.items.filter(i => i.id !== action.payload)
|
|
94
|
-
return { ...state, items, total: calculateTotal(items) }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
case 'UPDATE_QUANTITY': {
|
|
98
|
-
const items = state.items
|
|
99
|
-
.map(i => i.id === action.payload.id
|
|
100
|
-
? { ...i, quantity: action.payload.quantity }
|
|
101
|
-
: i
|
|
102
|
-
)
|
|
103
|
-
.filter(i => i.quantity > 0)
|
|
104
|
-
return { ...state, items, total: calculateTotal(items) }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
case 'CLEAR_CART':
|
|
108
|
-
return { items: [], total: 0 }
|
|
109
|
-
|
|
110
|
-
default:
|
|
111
|
-
return state
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function calculateTotal(items: CartItem[]): number {
|
|
116
|
-
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
interface CartContextType {
|
|
120
|
-
state: CartState
|
|
121
|
-
dispatch: Dispatch<CartAction>
|
|
122
|
-
addItem: (item: Omit<CartItem, 'quantity'>) => void
|
|
123
|
-
removeItem: (id: string) => void
|
|
124
|
-
updateQuantity: (id: string, quantity: number) => void
|
|
125
|
-
clearCart: () => void
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const CartContext = createContext<CartContextType | undefined>(undefined)
|
|
129
|
-
|
|
130
|
-
export function CartProvider({ children }: { children: ReactNode }) {
|
|
131
|
-
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 })
|
|
132
|
-
|
|
133
|
-
const addItem = (item: Omit<CartItem, 'quantity'>) =>
|
|
134
|
-
dispatch({ type: 'ADD_ITEM', payload: item })
|
|
135
|
-
|
|
136
|
-
const removeItem = (id: string) =>
|
|
137
|
-
dispatch({ type: 'REMOVE_ITEM', payload: id })
|
|
138
|
-
|
|
139
|
-
const updateQuantity = (id: string, quantity: number) =>
|
|
140
|
-
dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
|
|
141
|
-
|
|
142
|
-
const clearCart = () =>
|
|
143
|
-
dispatch({ type: 'CLEAR_CART' })
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<CartContext.Provider value={{
|
|
147
|
-
state, dispatch, addItem, removeItem, updateQuantity, clearCart
|
|
148
|
-
}}>
|
|
149
|
-
{children}
|
|
150
|
-
</CartContext.Provider>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function useCart() {
|
|
155
|
-
const context = useContext(CartContext)
|
|
156
|
-
if (!context) {
|
|
157
|
-
throw new Error('useCart must be used within a CartProvider')
|
|
158
|
-
}
|
|
159
|
-
return context
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## Optimized Context
|
|
164
|
-
|
|
165
|
-
```tsx
|
|
166
|
-
// contexts/OptimizedContext.tsx
|
|
167
|
-
// Split state and actions to prevent unnecessary re-renders
|
|
168
|
-
|
|
169
|
-
import { createContext, useContext, useState, useMemo, ReactNode } from 'react'
|
|
170
|
-
|
|
171
|
-
interface User {
|
|
172
|
-
id: string
|
|
173
|
-
name: string
|
|
174
|
-
email: string
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Separate contexts for state and actions
|
|
178
|
-
const UserStateContext = createContext<User | null>(null)
|
|
179
|
-
const UserActionsContext = createContext<{
|
|
180
|
-
setUser: (user: User | null) => void
|
|
181
|
-
updateUser: (updates: Partial<User>) => void
|
|
182
|
-
} | null>(null)
|
|
183
|
-
|
|
184
|
-
export function UserProvider({ children }: { children: ReactNode }) {
|
|
185
|
-
const [user, setUser] = useState<User | null>(null)
|
|
186
|
-
|
|
187
|
-
// Memoize actions to prevent re-renders
|
|
188
|
-
const actions = useMemo(() => ({
|
|
189
|
-
setUser,
|
|
190
|
-
updateUser: (updates: Partial<User>) =>
|
|
191
|
-
setUser(prev => prev ? { ...prev, ...updates } : null)
|
|
192
|
-
}), [])
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<UserStateContext.Provider value={user}>
|
|
196
|
-
<UserActionsContext.Provider value={actions}>
|
|
197
|
-
{children}
|
|
198
|
-
</UserActionsContext.Provider>
|
|
199
|
-
</UserStateContext.Provider>
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Separate hooks
|
|
204
|
-
export function useUser() {
|
|
205
|
-
const user = useContext(UserStateContext)
|
|
206
|
-
// null is a valid value, so we need a different check
|
|
207
|
-
return user
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function useUserActions() {
|
|
211
|
-
const context = useContext(UserActionsContext)
|
|
212
|
-
if (!context) {
|
|
213
|
-
throw new Error('useUserActions must be used within UserProvider')
|
|
214
|
-
}
|
|
215
|
-
return context
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Components that only read user won't re-render when actions change
|
|
219
|
-
function UserDisplay() {
|
|
220
|
-
const user = useUser()
|
|
221
|
-
return <span>{user?.name}</span>
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Components that only use actions won't re-render when user changes
|
|
225
|
-
function LogoutButton() {
|
|
226
|
-
const { setUser } = useUserActions()
|
|
227
|
-
return <button onClick={() => setUser(null)}>Logout</button>
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Composable Providers
|
|
232
|
-
|
|
233
|
-
```tsx
|
|
234
|
-
// app/providers.tsx
|
|
235
|
-
'use client'
|
|
236
|
-
|
|
237
|
-
import { ThemeProvider } from '@/contexts/ThemeContext'
|
|
238
|
-
import { UserProvider } from '@/contexts/UserContext'
|
|
239
|
-
import { CartProvider } from '@/contexts/CartContext'
|
|
240
|
-
import { QueryProvider } from '@/contexts/QueryContext'
|
|
241
|
-
import { ReactNode } from 'react'
|
|
242
|
-
|
|
243
|
-
type Provider = ({ children }: { children: ReactNode }) => JSX.Element
|
|
244
|
-
|
|
245
|
-
function composeProviders(...providers: Provider[]) {
|
|
246
|
-
return ({ children }: { children: ReactNode }) =>
|
|
247
|
-
providers.reduceRight(
|
|
248
|
-
(acc, Provider) => <Provider>{acc}</Provider>,
|
|
249
|
-
children
|
|
250
|
-
)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const Providers = composeProviders(
|
|
254
|
-
QueryProvider,
|
|
255
|
-
ThemeProvider,
|
|
256
|
-
UserProvider,
|
|
257
|
-
CartProvider
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
export default Providers
|
|
261
|
-
|
|
262
|
-
// app/layout.tsx
|
|
263
|
-
import Providers from './providers'
|
|
264
|
-
|
|
265
|
-
export default function RootLayout({ children }) {
|
|
266
|
-
return (
|
|
267
|
-
<html>
|
|
268
|
-
<body>
|
|
269
|
-
<Providers>{children}</Providers>
|
|
270
|
-
</body>
|
|
271
|
-
</html>
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
## Context with Local Storage
|
|
277
|
-
|
|
278
|
-
```tsx
|
|
279
|
-
// contexts/PersistentContext.tsx
|
|
280
|
-
'use client'
|
|
281
|
-
|
|
282
|
-
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
|
|
283
|
-
|
|
284
|
-
interface Settings {
|
|
285
|
-
notifications: boolean
|
|
286
|
-
language: string
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const defaultSettings: Settings = {
|
|
290
|
-
notifications: true,
|
|
291
|
-
language: 'en'
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const SettingsContext = createContext<{
|
|
295
|
-
settings: Settings
|
|
296
|
-
updateSettings: (updates: Partial<Settings>) => void
|
|
297
|
-
} | null>(null)
|
|
298
|
-
|
|
299
|
-
export function SettingsProvider({ children }: { children: ReactNode }) {
|
|
300
|
-
const [settings, setSettings] = useState<Settings>(defaultSettings)
|
|
301
|
-
const [mounted, setMounted] = useState(false)
|
|
302
|
-
|
|
303
|
-
// Load from localStorage on mount
|
|
304
|
-
useEffect(() => {
|
|
305
|
-
const stored = localStorage.getItem('settings')
|
|
306
|
-
if (stored) {
|
|
307
|
-
setSettings(JSON.parse(stored))
|
|
308
|
-
}
|
|
309
|
-
setMounted(true)
|
|
310
|
-
}, [])
|
|
311
|
-
|
|
312
|
-
// Persist to localStorage on change
|
|
313
|
-
useEffect(() => {
|
|
314
|
-
if (mounted) {
|
|
315
|
-
localStorage.setItem('settings', JSON.stringify(settings))
|
|
316
|
-
}
|
|
317
|
-
}, [settings, mounted])
|
|
318
|
-
|
|
319
|
-
const updateSettings = (updates: Partial<Settings>) => {
|
|
320
|
-
setSettings(prev => ({ ...prev, ...updates }))
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Prevent hydration mismatch
|
|
324
|
-
if (!mounted) {
|
|
325
|
-
return null
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return (
|
|
329
|
-
<SettingsContext.Provider value={{ settings, updateSettings }}>
|
|
330
|
-
{children}
|
|
331
|
-
</SettingsContext.Provider>
|
|
332
|
-
)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
export function useSettings() {
|
|
336
|
-
const context = useContext(SettingsContext)
|
|
337
|
-
if (!context) {
|
|
338
|
-
throw new Error('useSettings must be used within SettingsProvider')
|
|
339
|
-
}
|
|
340
|
-
return context
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
## When to Use
|
|
345
|
-
|
|
346
|
-
- Theme/preferences
|
|
347
|
-
- Auth state
|
|
348
|
-
- Shopping cart
|
|
349
|
-
- Feature flags
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
# React Query Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for server state management with TanStack Query.
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
// app/providers.tsx
|
|
9
|
-
'use client'
|
|
10
|
-
|
|
11
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
12
|
-
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
|
13
|
-
import { useState } from 'react'
|
|
14
|
-
|
|
15
|
-
export function Providers({ children }: { children: React.ReactNode }) {
|
|
16
|
-
const [queryClient] = useState(() => new QueryClient({
|
|
17
|
-
defaultOptions: {
|
|
18
|
-
queries: {
|
|
19
|
-
staleTime: 60 * 1000, // 1 minute
|
|
20
|
-
refetchOnWindowFocus: false
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}))
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<QueryClientProvider client={queryClient}>
|
|
27
|
-
{children}
|
|
28
|
-
<ReactQueryDevtools initialIsOpen={false} />
|
|
29
|
-
</QueryClientProvider>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Basic Query
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// hooks/useUsers.ts
|
|
38
|
-
import { useQuery } from '@tanstack/react-query'
|
|
39
|
-
|
|
40
|
-
interface User {
|
|
41
|
-
id: string
|
|
42
|
-
name: string
|
|
43
|
-
email: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function fetchUsers(): Promise<User[]> {
|
|
47
|
-
const response = await fetch('/api/users')
|
|
48
|
-
if (!response.ok) throw new Error('Failed to fetch users')
|
|
49
|
-
return response.json()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function useUsers() {
|
|
53
|
-
return useQuery({
|
|
54
|
-
queryKey: ['users'],
|
|
55
|
-
queryFn: fetchUsers
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Usage
|
|
60
|
-
function UserList() {
|
|
61
|
-
const { data: users, isLoading, error } = useUsers()
|
|
62
|
-
|
|
63
|
-
if (isLoading) return <Skeleton />
|
|
64
|
-
if (error) return <Error message={error.message} />
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<ul>
|
|
68
|
-
{users?.map(user => (
|
|
69
|
-
<li key={user.id}>{user.name}</li>
|
|
70
|
-
))}
|
|
71
|
-
</ul>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Parameterized Query
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
// hooks/useUser.ts
|
|
80
|
-
import { useQuery } from '@tanstack/react-query'
|
|
81
|
-
|
|
82
|
-
async function fetchUser(id: string): Promise<User> {
|
|
83
|
-
const response = await fetch(`/api/users/${id}`)
|
|
84
|
-
if (!response.ok) throw new Error('User not found')
|
|
85
|
-
return response.json()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function useUser(id: string) {
|
|
89
|
-
return useQuery({
|
|
90
|
-
queryKey: ['users', id],
|
|
91
|
-
queryFn: () => fetchUser(id),
|
|
92
|
-
enabled: !!id // Only fetch when id exists
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// With options
|
|
97
|
-
export function useUserWithOptions(id: string, options?: { enabled?: boolean }) {
|
|
98
|
-
return useQuery({
|
|
99
|
-
queryKey: ['users', id],
|
|
100
|
-
queryFn: () => fetchUser(id),
|
|
101
|
-
enabled: options?.enabled !== false && !!id,
|
|
102
|
-
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
103
|
-
gcTime: 10 * 60 * 1000 // 10 minutes (formerly cacheTime)
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Mutations
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// hooks/useCreateUser.ts
|
|
112
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
113
|
-
|
|
114
|
-
interface CreateUserData {
|
|
115
|
-
name: string
|
|
116
|
-
email: string
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function createUser(data: CreateUserData): Promise<User> {
|
|
120
|
-
const response = await fetch('/api/users', {
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: { 'Content-Type': 'application/json' },
|
|
123
|
-
body: JSON.stringify(data)
|
|
124
|
-
})
|
|
125
|
-
if (!response.ok) throw new Error('Failed to create user')
|
|
126
|
-
return response.json()
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function useCreateUser() {
|
|
130
|
-
const queryClient = useQueryClient()
|
|
131
|
-
|
|
132
|
-
return useMutation({
|
|
133
|
-
mutationFn: createUser,
|
|
134
|
-
onSuccess: (newUser) => {
|
|
135
|
-
// Invalidate and refetch users list
|
|
136
|
-
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
137
|
-
|
|
138
|
-
// Or optimistically add to cache
|
|
139
|
-
queryClient.setQueryData(['users'], (old: User[] | undefined) =>
|
|
140
|
-
old ? [...old, newUser] : [newUser]
|
|
141
|
-
)
|
|
142
|
-
},
|
|
143
|
-
onError: (error) => {
|
|
144
|
-
console.error('Failed to create user:', error)
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Usage
|
|
150
|
-
function CreateUserForm() {
|
|
151
|
-
const createUser = useCreateUser()
|
|
152
|
-
|
|
153
|
-
const handleSubmit = (data: CreateUserData) => {
|
|
154
|
-
createUser.mutate(data, {
|
|
155
|
-
onSuccess: () => {
|
|
156
|
-
toast.success('User created!')
|
|
157
|
-
}
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return (
|
|
162
|
-
<form onSubmit={handleSubmit}>
|
|
163
|
-
{/* ... */}
|
|
164
|
-
<button disabled={createUser.isPending}>
|
|
165
|
-
{createUser.isPending ? 'Creating...' : 'Create'}
|
|
166
|
-
</button>
|
|
167
|
-
</form>
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
## Optimistic Updates
|
|
173
|
-
|
|
174
|
-
```typescript
|
|
175
|
-
// hooks/useUpdateUser.ts
|
|
176
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
177
|
-
|
|
178
|
-
export function useUpdateUser() {
|
|
179
|
-
const queryClient = useQueryClient()
|
|
180
|
-
|
|
181
|
-
return useMutation({
|
|
182
|
-
mutationFn: updateUser,
|
|
183
|
-
onMutate: async (updatedUser) => {
|
|
184
|
-
// Cancel outgoing refetches
|
|
185
|
-
await queryClient.cancelQueries({ queryKey: ['users', updatedUser.id] })
|
|
186
|
-
|
|
187
|
-
// Snapshot previous value
|
|
188
|
-
const previousUser = queryClient.getQueryData(['users', updatedUser.id])
|
|
189
|
-
|
|
190
|
-
// Optimistically update
|
|
191
|
-
queryClient.setQueryData(['users', updatedUser.id], updatedUser)
|
|
192
|
-
|
|
193
|
-
// Return context with snapshot
|
|
194
|
-
return { previousUser }
|
|
195
|
-
},
|
|
196
|
-
onError: (err, updatedUser, context) => {
|
|
197
|
-
// Rollback on error
|
|
198
|
-
queryClient.setQueryData(
|
|
199
|
-
['users', updatedUser.id],
|
|
200
|
-
context?.previousUser
|
|
201
|
-
)
|
|
202
|
-
},
|
|
203
|
-
onSettled: (data, error, variables) => {
|
|
204
|
-
// Always refetch to ensure cache is in sync
|
|
205
|
-
queryClient.invalidateQueries({ queryKey: ['users', variables.id] })
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
## Infinite Query
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
// hooks/usePosts.ts
|
|
215
|
-
import { useInfiniteQuery } from '@tanstack/react-query'
|
|
216
|
-
|
|
217
|
-
interface PostsResponse {
|
|
218
|
-
posts: Post[]
|
|
219
|
-
nextCursor: string | null
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function fetchPosts({ pageParam }: { pageParam?: string }): Promise<PostsResponse> {
|
|
223
|
-
const url = pageParam
|
|
224
|
-
? `/api/posts?cursor=${pageParam}`
|
|
225
|
-
: '/api/posts'
|
|
226
|
-
|
|
227
|
-
const response = await fetch(url)
|
|
228
|
-
return response.json()
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export function useInfinitePosts() {
|
|
232
|
-
return useInfiniteQuery({
|
|
233
|
-
queryKey: ['posts'],
|
|
234
|
-
queryFn: fetchPosts,
|
|
235
|
-
initialPageParam: undefined,
|
|
236
|
-
getNextPageParam: (lastPage) => lastPage.nextCursor
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Usage
|
|
241
|
-
function PostFeed() {
|
|
242
|
-
const {
|
|
243
|
-
data,
|
|
244
|
-
fetchNextPage,
|
|
245
|
-
hasNextPage,
|
|
246
|
-
isFetchingNextPage
|
|
247
|
-
} = useInfinitePosts()
|
|
248
|
-
|
|
249
|
-
return (
|
|
250
|
-
<div>
|
|
251
|
-
{data?.pages.map((page, i) => (
|
|
252
|
-
<Fragment key={i}>
|
|
253
|
-
{page.posts.map(post => (
|
|
254
|
-
<PostCard key={post.id} post={post} />
|
|
255
|
-
))}
|
|
256
|
-
</Fragment>
|
|
257
|
-
))}
|
|
258
|
-
|
|
259
|
-
<button
|
|
260
|
-
onClick={() => fetchNextPage()}
|
|
261
|
-
disabled={!hasNextPage || isFetchingNextPage}
|
|
262
|
-
>
|
|
263
|
-
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more posts'}
|
|
264
|
-
</button>
|
|
265
|
-
</div>
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
## Prefetching
|
|
271
|
-
|
|
272
|
-
```typescript
|
|
273
|
-
// Prefetch on hover
|
|
274
|
-
function UserLink({ userId }: { userId: string }) {
|
|
275
|
-
const queryClient = useQueryClient()
|
|
276
|
-
|
|
277
|
-
const prefetchUser = () => {
|
|
278
|
-
queryClient.prefetchQuery({
|
|
279
|
-
queryKey: ['users', userId],
|
|
280
|
-
queryFn: () => fetchUser(userId)
|
|
281
|
-
})
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return (
|
|
285
|
-
<Link href={`/users/${userId}`} onMouseEnter={prefetchUser}>
|
|
286
|
-
View User
|
|
287
|
-
</Link>
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Prefetch in Server Component
|
|
292
|
-
async function UserPage({ params }: { params: { id: string } }) {
|
|
293
|
-
const queryClient = new QueryClient()
|
|
294
|
-
|
|
295
|
-
await queryClient.prefetchQuery({
|
|
296
|
-
queryKey: ['users', params.id],
|
|
297
|
-
queryFn: () => fetchUser(params.id)
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
return (
|
|
301
|
-
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
302
|
-
<UserProfile userId={params.id} />
|
|
303
|
-
</HydrationBoundary>
|
|
304
|
-
)
|
|
305
|
-
}
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
## When to Use
|
|
309
|
-
|
|
310
|
-
- Server state
|
|
311
|
-
- Data fetching
|
|
312
|
-
- Caching
|
|
313
|
-
- Optimistic updates
|