@mr.dj2u/knowledge 0.1.0
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/content/checklists/ship-test-loop.md +16 -0
- package/dist/content/checklists/unified-agent-bundle-validation.md +8 -0
- package/dist/content/examples/ship-test-loop.md +13 -0
- package/dist/content/examples/unified-agent-bundle-bootstrap.md +8 -0
- package/dist/content/guides/animation-performance.md +30 -0
- package/dist/content/guides/post-create-onboarding.md +113 -0
- package/dist/content/patterns/api/api-routes.md +314 -0
- package/dist/content/patterns/api/error-handling.md +311 -0
- package/dist/content/patterns/database/drizzle-schema.md +280 -0
- package/dist/content/patterns/database/migrations.md +365 -0
- package/dist/content/patterns/database/query-organization.md +537 -0
- package/dist/content/patterns/database/relations.md +450 -0
- package/dist/content/patterns/deployment/build-configuration.md +452 -0
- package/dist/content/patterns/deployment/ci-cd-patterns.md +448 -0
- package/dist/content/patterns/deployment/environment-config.md +380 -0
- package/dist/content/patterns/deployment/hosting-setup.md +425 -0
- package/dist/content/patterns/project/configuration-patterns.md +459 -0
- package/dist/content/patterns/project/documentation-org.md +506 -0
- package/dist/content/patterns/project/folder-structure.md +398 -0
- package/dist/content/patterns/project/library-exports.md +465 -0
- package/dist/content/patterns/project/monorepo-structure.md +500 -0
- package/dist/content/patterns/routing/dynamic-routes.md +221 -0
- package/dist/content/patterns/routing/file-based-routing.md +186 -0
- package/dist/content/patterns/routing/route-groups.md +429 -0
- package/dist/content/patterns/state/persistence-middleware.md +521 -0
- package/dist/content/patterns/state/selector-hooks.md +538 -0
- package/dist/content/patterns/state/store-organization.md +539 -0
- package/dist/content/patterns/state/zustand-patterns.md +348 -0
- package/dist/content/patterns/styling/component-styling.md +468 -0
- package/dist/content/patterns/styling/responsive-patterns.md +398 -0
- package/dist/content/patterns/styling/theme-configuration.md +426 -0
- package/dist/content/patterns/styling/uniwind-setup.md +412 -0
- package/dist/content/prompts/continue-development.md +27 -0
- package/dist/content/prompts/create-expo-super-stack.md +29 -0
- package/dist/content/prompts/fix-seo.md +29 -0
- package/dist/content/prompts/onboard-new-expo-app.md +11 -0
- package/dist/content/prompts/prepare-deploy.md +29 -0
- package/dist/content/prompts/project-research-plan.md +29 -0
- package/dist/content/prompts/review-expo-project.md +29 -0
- package/dist/content/prompts/run-doctor.md +30 -0
- package/dist/content/prompts/ship-test-loop.md +24 -0
- package/dist/content/reference/create-expo-stack-uniwind.md +29 -0
- package/dist/content/reference/doctor-dogfood.md +42 -0
- package/dist/content/reference/mcp-sdk-transport.md +30 -0
- package/dist/content/reference/package-ci-patterns.md +24 -0
- package/dist/content/reference/reference-repo-evacuation.md +31 -0
- package/dist/content/resource-index.json +67 -0
- package/dist/content/rules/app-folder-architecture.md +13 -0
- package/dist/content/rules/env-hygiene.md +9 -0
- package/dist/content/rules/seo-metadata.md +7 -0
- package/dist/content/rules/ssr-safety.md +9 -0
- package/dist/content/skills/api-routes.md +33 -0
- package/dist/content/skills/continue-development.md +31 -0
- package/dist/content/skills/debugging.md +31 -0
- package/dist/content/skills/deployment.md +32 -0
- package/dist/content/skills/dev-server-management.md +31 -0
- package/dist/content/skills/env-vars.md +32 -0
- package/dist/content/skills/expo-router-architecture.md +33 -0
- package/dist/content/skills/expo-ssr-safety.md +32 -0
- package/dist/content/skills/plugin-creation.md +41 -0
- package/dist/content/skills/production-server-patterns.md +31 -0
- package/dist/content/skills/project-onboarding.md +31 -0
- package/dist/content/skills/research-plan-intake.md +31 -0
- package/dist/content/skills/seo-metadata.md +31 -0
- package/dist/content/skills/super-stack-startup.md +31 -0
- package/dist/content/skills/uniwind-theming.md +32 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +369 -0
- package/dist/index.js.map +1 -0
- package/dist/patterns/index.d.ts +78 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +264 -0
- package/dist/patterns/index.js.map +1 -0
- package/dist/prompts/index.d.ts +35 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +270 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/skills/index.d.ts +3 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/index.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
# Store Organization Pattern
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Store organization structures Zustand stores into logically separated, focused stores rather than monolithic state objects. Each store manages a specific domain (auth, UI, features) with clear separation of concerns, improving maintainability, performance, and reusability.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
**Use store organization** for:
|
|
10
|
+
- ✅ Separating auth/user state from app state
|
|
11
|
+
- ✅ Feature-specific stores (dex tracker, favorites, settings)
|
|
12
|
+
- ✅ UI state isolation (modals, navigation stacks)
|
|
13
|
+
- ✅ Preventing unnecessary re-renders across unrelated state changes
|
|
14
|
+
- ✅ Reusable store composition across projects
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
**One store per domain:**
|
|
19
|
+
- `useAuthStore` - User authentication, profile, session
|
|
20
|
+
- `useUIStore` - Modals, toasts, navigation state
|
|
21
|
+
- `useFeatureStore` - Feature-specific data (favorites, tracking)
|
|
22
|
+
- `useSettingsStore` - User preferences, app configuration
|
|
23
|
+
|
|
24
|
+
**Tight cohesion, loose coupling:**
|
|
25
|
+
- Each store manages its own domain exclusively
|
|
26
|
+
- Stores don't depend on each other
|
|
27
|
+
- Actions within a store are self-contained
|
|
28
|
+
- Cross-store operations handled at component/hook level
|
|
29
|
+
|
|
30
|
+
## Code Example
|
|
31
|
+
|
|
32
|
+
### Basic Store Structure
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// File: src/store/authStore.ts
|
|
36
|
+
import { create } from 'zustand';
|
|
37
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
38
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
39
|
+
|
|
40
|
+
interface User {
|
|
41
|
+
id: string;
|
|
42
|
+
email: string;
|
|
43
|
+
username: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Profile {
|
|
47
|
+
id: string;
|
|
48
|
+
displayName: string;
|
|
49
|
+
avatar?: string;
|
|
50
|
+
bio?: string;
|
|
51
|
+
ageVerified: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Session {
|
|
55
|
+
accessToken: string;
|
|
56
|
+
refreshToken: string;
|
|
57
|
+
expiresAt: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface AuthState {
|
|
61
|
+
// State
|
|
62
|
+
user: User | null;
|
|
63
|
+
profile: Profile | null;
|
|
64
|
+
session: Session | null;
|
|
65
|
+
isLoggedIn: boolean;
|
|
66
|
+
loading: boolean;
|
|
67
|
+
error: string | null;
|
|
68
|
+
|
|
69
|
+
// Computed properties
|
|
70
|
+
isAdult: boolean;
|
|
71
|
+
canUseSocialFeatures: boolean;
|
|
72
|
+
|
|
73
|
+
// Actions
|
|
74
|
+
setUser: (user: User | null) => void;
|
|
75
|
+
setProfile: (profile: Profile | null) => void;
|
|
76
|
+
setSession: (session: Session | null) => void;
|
|
77
|
+
setLoading: (loading: boolean) => void;
|
|
78
|
+
setError: (error: string | null) => void;
|
|
79
|
+
signOut: () => Promise<void>;
|
|
80
|
+
initializeAuth: () => Promise<void>;
|
|
81
|
+
reset: () => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const useAuthStore = create<AuthState>()(
|
|
85
|
+
persist(
|
|
86
|
+
(set, get) => ({
|
|
87
|
+
// Initial state
|
|
88
|
+
user: null,
|
|
89
|
+
profile: null,
|
|
90
|
+
session: null,
|
|
91
|
+
isLoggedIn: false,
|
|
92
|
+
loading: true,
|
|
93
|
+
error: null,
|
|
94
|
+
|
|
95
|
+
// Computed properties (getters)
|
|
96
|
+
get isAdult(): boolean {
|
|
97
|
+
const profile = get().profile;
|
|
98
|
+
if (!profile?.ageVerified) return false;
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
get canUseSocialFeatures(): boolean {
|
|
103
|
+
return get().isLoggedIn && get().isAdult;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Actions
|
|
107
|
+
setUser: (user) => {
|
|
108
|
+
set({ user, isLoggedIn: !!user, error: null });
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
setProfile: (profile) => {
|
|
112
|
+
set({ profile, error: null });
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
setSession: (session) => {
|
|
116
|
+
set({ session, error: null });
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
setLoading: (loading) => {
|
|
120
|
+
set({ loading });
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
setError: (error) => {
|
|
124
|
+
set({ error });
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
signOut: async () => {
|
|
128
|
+
try {
|
|
129
|
+
set({ loading: true });
|
|
130
|
+
// Call logout API
|
|
131
|
+
await supabase.auth.signOut();
|
|
132
|
+
|
|
133
|
+
set({
|
|
134
|
+
user: null,
|
|
135
|
+
profile: null,
|
|
136
|
+
session: null,
|
|
137
|
+
isLoggedIn: false,
|
|
138
|
+
loading: false,
|
|
139
|
+
error: null,
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
set({
|
|
143
|
+
error: error instanceof Error ? error.message : 'Logout failed',
|
|
144
|
+
loading: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
initializeAuth: async () => {
|
|
150
|
+
try {
|
|
151
|
+
set({ loading: true });
|
|
152
|
+
// Check session, load user and profile
|
|
153
|
+
const { data } = await supabase.auth.getSession();
|
|
154
|
+
|
|
155
|
+
if (data.session) {
|
|
156
|
+
set({
|
|
157
|
+
session: {
|
|
158
|
+
accessToken: data.session.access_token,
|
|
159
|
+
refreshToken: data.session.refresh_token || '',
|
|
160
|
+
expiresAt: data.session.expires_at || 0,
|
|
161
|
+
},
|
|
162
|
+
isLoggedIn: true,
|
|
163
|
+
loading: false,
|
|
164
|
+
});
|
|
165
|
+
// Load user and profile...
|
|
166
|
+
} else {
|
|
167
|
+
set({ isLoggedIn: false, loading: false });
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
set({
|
|
171
|
+
error: error instanceof Error ? error.message : 'Initialization failed',
|
|
172
|
+
loading: false,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
reset: () => {
|
|
178
|
+
set({
|
|
179
|
+
user: null,
|
|
180
|
+
profile: null,
|
|
181
|
+
session: null,
|
|
182
|
+
isLoggedIn: false,
|
|
183
|
+
loading: true,
|
|
184
|
+
error: null,
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
{
|
|
189
|
+
name: 'auth-storage',
|
|
190
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
191
|
+
// Partial persistence - don't persist loading or error states
|
|
192
|
+
partialize: (state) => ({
|
|
193
|
+
user: state.user,
|
|
194
|
+
profile: state.profile,
|
|
195
|
+
session: state.session,
|
|
196
|
+
}),
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### UI State Store
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// File: src/store/uiStore.ts
|
|
206
|
+
import { create } from 'zustand';
|
|
207
|
+
|
|
208
|
+
interface Toast {
|
|
209
|
+
id: string;
|
|
210
|
+
message: string;
|
|
211
|
+
type: 'success' | 'error' | 'info';
|
|
212
|
+
duration?: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface UIState {
|
|
216
|
+
// Modals
|
|
217
|
+
modals: Record<string, boolean>;
|
|
218
|
+
openModal: (id: string) => void;
|
|
219
|
+
closeModal: (id: string) => void;
|
|
220
|
+
|
|
221
|
+
// Toasts
|
|
222
|
+
toasts: Toast[];
|
|
223
|
+
showToast: (message: string, type: Toast['type'], duration?: number) => void;
|
|
224
|
+
dismissToast: (id: string) => void;
|
|
225
|
+
|
|
226
|
+
// Loading states
|
|
227
|
+
globalLoading: boolean;
|
|
228
|
+
setGlobalLoading: (loading: boolean) => void;
|
|
229
|
+
|
|
230
|
+
// Theme
|
|
231
|
+
darkMode: boolean;
|
|
232
|
+
toggleDarkMode: () => void;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const useUIStore = create<UIState>((set, get) => ({
|
|
236
|
+
// Modals
|
|
237
|
+
modals: {},
|
|
238
|
+
openModal: (id) => {
|
|
239
|
+
set((state) => ({
|
|
240
|
+
modals: { ...state.modals, [id]: true },
|
|
241
|
+
}));
|
|
242
|
+
},
|
|
243
|
+
closeModal: (id) => {
|
|
244
|
+
set((state) => ({
|
|
245
|
+
modals: { ...state.modals, [id]: false },
|
|
246
|
+
}));
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
// Toasts
|
|
250
|
+
toasts: [],
|
|
251
|
+
showToast: (message, type, duration = 3000) => {
|
|
252
|
+
const id = `${Date.now()}`;
|
|
253
|
+
set((state) => ({
|
|
254
|
+
toasts: [...state.toasts, { id, message, type, duration }],
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
// Auto-dismiss
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
get().dismissToast(id);
|
|
260
|
+
}, duration);
|
|
261
|
+
},
|
|
262
|
+
dismissToast: (id) => {
|
|
263
|
+
set((state) => ({
|
|
264
|
+
toasts: state.toasts.filter((t) => t.id !== id),
|
|
265
|
+
}));
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// Loading
|
|
269
|
+
globalLoading: false,
|
|
270
|
+
setGlobalLoading: (loading) => set({ globalLoading: loading }),
|
|
271
|
+
|
|
272
|
+
// Theme
|
|
273
|
+
darkMode: false,
|
|
274
|
+
toggleDarkMode: () => {
|
|
275
|
+
set((state) => ({ darkMode: !state.darkMode }));
|
|
276
|
+
},
|
|
277
|
+
}));
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Feature-Specific Store
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// File: src/store/dexTrackerStore.ts
|
|
284
|
+
import { create } from 'zustand';
|
|
285
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
286
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
287
|
+
|
|
288
|
+
interface DexTrackerState {
|
|
289
|
+
// State
|
|
290
|
+
trackedPokemon: Record<number, boolean>;
|
|
291
|
+
shinyPokemon: Record<number, boolean>;
|
|
292
|
+
favoritesList: number[];
|
|
293
|
+
|
|
294
|
+
// Computed
|
|
295
|
+
totalCaught: () => number;
|
|
296
|
+
totalShiny: () => number;
|
|
297
|
+
completionPercentage: () => number;
|
|
298
|
+
isCaught: (dexNumber: number) => boolean;
|
|
299
|
+
isShiny: (dexNumber: number) => boolean;
|
|
300
|
+
isFavorite: (dexNumber: number) => boolean;
|
|
301
|
+
|
|
302
|
+
// Actions
|
|
303
|
+
addPokemon: (dexNumber: number) => void;
|
|
304
|
+
removePokemon: (dexNumber: number) => void;
|
|
305
|
+
toggleShiny: (dexNumber: number) => void;
|
|
306
|
+
addFavorite: (dexNumber: number) => void;
|
|
307
|
+
removeFavorite: (dexNumber: number) => void;
|
|
308
|
+
resetTracker: () => void;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export const useDexTrackerStore = create<DexTrackerState>()(
|
|
312
|
+
persist(
|
|
313
|
+
(set, get) => ({
|
|
314
|
+
trackedPokemon: {},
|
|
315
|
+
shinyPokemon: {},
|
|
316
|
+
favoritesList: [],
|
|
317
|
+
|
|
318
|
+
// Computed (getters)
|
|
319
|
+
totalCaught: () => Object.keys(get().trackedPokemon).length,
|
|
320
|
+
totalShiny: () => Object.keys(get().shinyPokemon).length,
|
|
321
|
+
completionPercentage: () => (get().totalCaught() / 1025) * 100,
|
|
322
|
+
isCaught: (dexNumber) => !!get().trackedPokemon[dexNumber],
|
|
323
|
+
isShiny: (dexNumber) => !!get().shinyPokemon[dexNumber],
|
|
324
|
+
isFavorite: (dexNumber) => get().favoritesList.includes(dexNumber),
|
|
325
|
+
|
|
326
|
+
// Actions
|
|
327
|
+
addPokemon: (dexNumber) => {
|
|
328
|
+
set((state) => ({
|
|
329
|
+
trackedPokemon: {
|
|
330
|
+
...state.trackedPokemon,
|
|
331
|
+
[dexNumber]: true,
|
|
332
|
+
},
|
|
333
|
+
}));
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
removePokemon: (dexNumber) => {
|
|
337
|
+
set((state) => {
|
|
338
|
+
const { [dexNumber]: _, ...rest } = state.trackedPokemon;
|
|
339
|
+
return { trackedPokemon: rest };
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
toggleShiny: (dexNumber) => {
|
|
344
|
+
set((state) => ({
|
|
345
|
+
shinyPokemon: {
|
|
346
|
+
...state.shinyPokemon,
|
|
347
|
+
[dexNumber]: !state.shinyPokemon[dexNumber],
|
|
348
|
+
},
|
|
349
|
+
}));
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
addFavorite: (dexNumber) => {
|
|
353
|
+
set((state) => {
|
|
354
|
+
if (state.favoritesList.includes(dexNumber)) {
|
|
355
|
+
return state;
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
favoritesList: [...state.favoritesList, dexNumber],
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
removeFavorite: (dexNumber) => {
|
|
364
|
+
set((state) => ({
|
|
365
|
+
favoritesList: state.favoritesList.filter((id) => id !== dexNumber),
|
|
366
|
+
}));
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
resetTracker: () => {
|
|
370
|
+
set({
|
|
371
|
+
trackedPokemon: {},
|
|
372
|
+
shinyPokemon: {},
|
|
373
|
+
favoritesList: [],
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
{
|
|
378
|
+
name: 'dex-tracker-storage',
|
|
379
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Multi-Store Architecture
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// File: src/store/index.ts
|
|
389
|
+
// Central export point for all stores
|
|
390
|
+
|
|
391
|
+
export { useAuthStore } from './authStore';
|
|
392
|
+
export { useUIStore } from './uiStore';
|
|
393
|
+
export { useDexTrackerStore } from './dexTrackerStore';
|
|
394
|
+
export { useSettingsStore } from './settingsStore';
|
|
395
|
+
export { useFavoritesStore } from './favoritesStore';
|
|
396
|
+
|
|
397
|
+
// Usage in components
|
|
398
|
+
import { useAuthStore, useUIStore, useDexTrackerStore } from '@/store';
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Store Usage Patterns
|
|
402
|
+
|
|
403
|
+
### Using Stores in Components
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
// File: src/components/UserProfile.tsx
|
|
407
|
+
import { useAuthStore } from '@/store/authStore';
|
|
408
|
+
|
|
409
|
+
export function UserProfile() {
|
|
410
|
+
// Using selector hook for performance (only re-render on email change)
|
|
411
|
+
const userEmail = useAuthStore((state) => state.user?.email);
|
|
412
|
+
|
|
413
|
+
return <Text>{userEmail}</Text>;
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Cross-Store Operations
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// File: src/hooks/useLogout.ts
|
|
421
|
+
import { useAuthStore } from '@/store/authStore';
|
|
422
|
+
import { useUIStore } from '@/store/uiStore';
|
|
423
|
+
import { useDexTrackerStore } from '@/store/dexTrackerStore';
|
|
424
|
+
|
|
425
|
+
export function useLogout() {
|
|
426
|
+
const signOut = useAuthStore((state) => state.signOut);
|
|
427
|
+
const resetTracker = useDexTrackerStore((state) => state.resetTracker);
|
|
428
|
+
const showToast = useUIStore((state) => state.showToast);
|
|
429
|
+
|
|
430
|
+
return async () => {
|
|
431
|
+
try {
|
|
432
|
+
await signOut();
|
|
433
|
+
resetTracker();
|
|
434
|
+
showToast('Logged out successfully', 'success');
|
|
435
|
+
} catch (error) {
|
|
436
|
+
showToast('Logout failed', 'error');
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Store Composition
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
// File: src/hooks/useAppState.ts
|
|
446
|
+
// Custom hook that combines multiple stores
|
|
447
|
+
|
|
448
|
+
export function useAppState() {
|
|
449
|
+
const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
|
|
450
|
+
const user = useAuthStore((state) => state.user);
|
|
451
|
+
const profile = useAuthStore((state) => state.profile);
|
|
452
|
+
const darkMode = useUIStore((state) => state.darkMode);
|
|
453
|
+
const totalCaught = useDexTrackerStore((state) => state.totalCaught());
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
isLoggedIn,
|
|
457
|
+
user,
|
|
458
|
+
profile,
|
|
459
|
+
darkMode,
|
|
460
|
+
totalCaught,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Store Organization Best Practices
|
|
466
|
+
|
|
467
|
+
### ✅ DO
|
|
468
|
+
|
|
469
|
+
1. **Keep stores focused on single domain**
|
|
470
|
+
```typescript
|
|
471
|
+
// ✅ GOOD
|
|
472
|
+
useAuthStore // Auth only
|
|
473
|
+
useUIStore // UI state only
|
|
474
|
+
useDexStore // Feature only
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
2. **Use partial persistence**
|
|
478
|
+
```typescript
|
|
479
|
+
partialize: (state) => ({
|
|
480
|
+
user: state.user, // Persist
|
|
481
|
+
profile: state.profile, // Persist
|
|
482
|
+
loading: state.loading, // DON'T persist (transient)
|
|
483
|
+
error: state.error, // DON'T persist (transient)
|
|
484
|
+
})
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
3. **Provide computed properties as getters**
|
|
488
|
+
```typescript
|
|
489
|
+
get isAdult(): boolean {
|
|
490
|
+
return this.profile?.ageVerified " false;
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### ❌ DON'T
|
|
495
|
+
|
|
496
|
+
1. **Don't create monolithic stores**
|
|
497
|
+
```typescript
|
|
498
|
+
// ❌ WRONG
|
|
499
|
+
useAppStore // Auth + UI + Features + Settings - too much!
|
|
500
|
+
|
|
501
|
+
// ✅ RIGHT
|
|
502
|
+
useAuthStore
|
|
503
|
+
useUIStore
|
|
504
|
+
useFeaturesStore
|
|
505
|
+
useSettingsStore
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
2. **Don't duplicate state across stores**
|
|
509
|
+
```typescript
|
|
510
|
+
// ❌ WRONG
|
|
511
|
+
// useAuthStore has user
|
|
512
|
+
// useUIStore also has user (duplicated)
|
|
513
|
+
|
|
514
|
+
// ✅ RIGHT
|
|
515
|
+
// Only useAuthStore has user
|
|
516
|
+
// useUIStore can import it via selectors
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
3. **Don't make stores depend on each other**
|
|
520
|
+
```typescript
|
|
521
|
+
// ❌ WRONG
|
|
522
|
+
export const useFeatureStore = create((set, get) => ({
|
|
523
|
+
signOut: useAuthStore.getState().signOut, // Circular dependency
|
|
524
|
+
}))
|
|
525
|
+
|
|
526
|
+
// ✅ RIGHT
|
|
527
|
+
// Composed in hooks/components, not in stores
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## Related Patterns
|
|
531
|
+
|
|
532
|
+
- [Zustand Patterns](./zustand-patterns.md) — Zustand fundamentals
|
|
533
|
+
- [Persistence Middleware](./persistence-middleware.md) — Persistence setup
|
|
534
|
+
- [Selector Hooks](./selector-hooks.md) — Performance optimization
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
*Pattern extracted from production repositories: core-monorepo, PokePages, DJsPortfolio*
|
|
539
|
+
*Files: src/store/ directory structures*
|