@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,538 @@
|
|
|
1
|
+
# Selector Hooks Pattern
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Selector hooks extract and memoize specific state values from Zustand stores, preventing unnecessary re-renders when unrelated store state changes. This pattern optimizes performance by ensuring components only re-render when *their specific selected state* changes, not when any store property updates.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
**Use selector hooks** when:
|
|
10
|
+
- ✅ Component only needs 1-2 properties from store (not entire store)
|
|
11
|
+
- ✅ Large stores with many independent properties
|
|
12
|
+
- ✅ Multiple components accessing different store slices
|
|
13
|
+
- ✅ Preventing cascade re-renders in lists
|
|
14
|
+
- ✅ Optimizing performance-critical components
|
|
15
|
+
- ✅ Creating reusable state accessors
|
|
16
|
+
|
|
17
|
+
## Core Concepts
|
|
18
|
+
|
|
19
|
+
**The Problem: Store Without Selectors**
|
|
20
|
+
```typescript
|
|
21
|
+
// ❌ WRONG - Component re-renders when ANY store property changes
|
|
22
|
+
function UserCard() {
|
|
23
|
+
const store = useAuthStore(); // Entire store destructured
|
|
24
|
+
// Component re-renders when user, profile, session, loading, error change
|
|
25
|
+
return <Text>{store.user?.email}</Text>;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**The Solution: Selector Hooks**
|
|
30
|
+
```typescript
|
|
31
|
+
// ✅ RIGHT - Component only re-renders when user changes
|
|
32
|
+
function UserCard() {
|
|
33
|
+
const userEmail = useAuthStore((state) => state.user?.email);
|
|
34
|
+
// Component re-renders ONLY when email changes
|
|
35
|
+
return <Text>{userEmail}</Text>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Selector Execution:**
|
|
40
|
+
1. Component calls selector hook
|
|
41
|
+
2. Selector function receives entire state
|
|
42
|
+
3. Selector returns only needed property(s)
|
|
43
|
+
4. Zustand compares returned value with previous
|
|
44
|
+
5. Re-render only if value changed (shallow equality)
|
|
45
|
+
|
|
46
|
+
## Code Examples
|
|
47
|
+
|
|
48
|
+
### Basic Selector Pattern
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// File: src/store/authStore.ts
|
|
52
|
+
import { create } from 'zustand';
|
|
53
|
+
|
|
54
|
+
interface AuthState {
|
|
55
|
+
user: User | null;
|
|
56
|
+
profile: Profile | null;
|
|
57
|
+
isLoggedIn: boolean;
|
|
58
|
+
loading: boolean;
|
|
59
|
+
error: string | null;
|
|
60
|
+
|
|
61
|
+
setUser: (user: User | null) => void;
|
|
62
|
+
login: (email: string, password: string) => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
66
|
+
user: null,
|
|
67
|
+
profile: null,
|
|
68
|
+
isLoggedIn: false,
|
|
69
|
+
loading: false,
|
|
70
|
+
error: null,
|
|
71
|
+
|
|
72
|
+
setUser: (user) => set({ user, isLoggedIn: !!user }),
|
|
73
|
+
login: async (email, password) => {
|
|
74
|
+
set({ loading: true });
|
|
75
|
+
// ...
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// File: src/hooks/useAuth.ts
|
|
80
|
+
/**
|
|
81
|
+
* Individual selector hooks for auth state
|
|
82
|
+
* Each hook selects one specific property
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
export const useUserEmail = () => {
|
|
86
|
+
return useAuthStore((state) => state.user?.email);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const useIsLoggedIn = () => {
|
|
90
|
+
return useAuthStore((state) => state.isLoggedIn);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const useAuthLoading = () => {
|
|
94
|
+
return useAuthStore((state) => state.loading);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const useAuthError = () => {
|
|
98
|
+
return useAuthStore((state) => state.error);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const useUserProfile = () => {
|
|
102
|
+
return useAuthStore((state) => state.profile);
|
|
103
|
+
};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Composite Selector Pattern
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// File: src/hooks/useAuth.ts
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Combine multiple related properties
|
|
113
|
+
* Useful when component needs several related values
|
|
114
|
+
*/
|
|
115
|
+
export const useAuthStatus = () => {
|
|
116
|
+
return useAuthStore((state) => ({
|
|
117
|
+
isLoggedIn: state.isLoggedIn,
|
|
118
|
+
loading: state.loading,
|
|
119
|
+
error: state.error,
|
|
120
|
+
}));
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* For object properties, use shallow equality check
|
|
125
|
+
*/
|
|
126
|
+
import { useShallow } from 'zustand/react';
|
|
127
|
+
|
|
128
|
+
export const useUserData = () => {
|
|
129
|
+
return useAuthStore(
|
|
130
|
+
useShallow((state) => ({
|
|
131
|
+
user: state.user,
|
|
132
|
+
profile: state.profile,
|
|
133
|
+
}))
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Computed/derived selectors
|
|
139
|
+
* Create values on-the-fly from state
|
|
140
|
+
*/
|
|
141
|
+
export const useIsAdmin = () => {
|
|
142
|
+
return useAuthStore((state) => {
|
|
143
|
+
return state.profile?.role === 'admin';
|
|
144
|
+
});
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const useUserDisplayName = () => {
|
|
148
|
+
return useAuthStore((state) => {
|
|
149
|
+
return state.user?.displayName || state.user?.email || 'Anonymous';
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Advanced: Memoized Selectors
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// File: src/hooks/useAuth.ts
|
|
158
|
+
import { useMemo } from 'react';
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Selector factory - returns memoized selector function
|
|
162
|
+
* Useful for passing parameters to selectors
|
|
163
|
+
*/
|
|
164
|
+
export function useUserById(userId: string) {
|
|
165
|
+
return useAuthStore(
|
|
166
|
+
useMemo(
|
|
167
|
+
() => (state) => {
|
|
168
|
+
// Selector only recreated if userId changes
|
|
169
|
+
if (state.user?.id === userId) {
|
|
170
|
+
return state.user;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
},
|
|
174
|
+
[userId]
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Selector with default value
|
|
181
|
+
*/
|
|
182
|
+
export const useThemePreference = (defaultTheme = 'auto') => {
|
|
183
|
+
return useAuthStore((state) => state.profile?.themePreference " defaultTheme);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Conditional selectors
|
|
188
|
+
*/
|
|
189
|
+
export const useLoggedInUserProfile = () => {
|
|
190
|
+
return useAuthStore((state) => {
|
|
191
|
+
// Only return if user is logged in
|
|
192
|
+
if (state.isLoggedIn && state.user && state.profile) {
|
|
193
|
+
return state.profile;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Action Selector Pattern
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// File: src/store/authStore.ts & hooks/useAuth.ts
|
|
204
|
+
|
|
205
|
+
interface AuthState {
|
|
206
|
+
// ... state properties
|
|
207
|
+
user: User | null;
|
|
208
|
+
profile: Profile | null;
|
|
209
|
+
|
|
210
|
+
// Actions
|
|
211
|
+
setUser: (user: User | null) => void;
|
|
212
|
+
login: (email: string, password: string) => Promise<void>;
|
|
213
|
+
logout: () => Promise<void>;
|
|
214
|
+
updateProfile: (profile: Partial<Profile>) => Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
218
|
+
// ... state
|
|
219
|
+
setUser: (user) => set({ user }),
|
|
220
|
+
login: async (email, password) => {
|
|
221
|
+
// ...
|
|
222
|
+
},
|
|
223
|
+
logout: async () => {
|
|
224
|
+
// ...
|
|
225
|
+
},
|
|
226
|
+
updateProfile: async (profile) => {
|
|
227
|
+
// ...
|
|
228
|
+
},
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Selector for actions
|
|
233
|
+
* Actions are stable references, no memoization needed
|
|
234
|
+
*/
|
|
235
|
+
export const useAuthActions = () => {
|
|
236
|
+
return useAuthStore((state) => ({
|
|
237
|
+
login: state.login,
|
|
238
|
+
logout: state.logout,
|
|
239
|
+
setUser: state.setUser,
|
|
240
|
+
}));
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Combined selectors: state + actions
|
|
245
|
+
*/
|
|
246
|
+
export const useAuthContext = () => {
|
|
247
|
+
return useAuthStore((state) => ({
|
|
248
|
+
// State
|
|
249
|
+
user: state.user,
|
|
250
|
+
isLoggedIn: state.isLoggedIn,
|
|
251
|
+
loading: state.loading,
|
|
252
|
+
error: state.error,
|
|
253
|
+
|
|
254
|
+
// Actions
|
|
255
|
+
login: state.login,
|
|
256
|
+
logout: state.logout,
|
|
257
|
+
setUser: state.setUser,
|
|
258
|
+
}));
|
|
259
|
+
};
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### List/Array Selectors
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// File: src/store/postsStore.ts & hooks/usePosts.ts
|
|
266
|
+
|
|
267
|
+
interface PostsState {
|
|
268
|
+
posts: Post[];
|
|
269
|
+
selectedPostId: string | null;
|
|
270
|
+
filter: 'all' | 'favorites' | 'archived';
|
|
271
|
+
|
|
272
|
+
addPost: (post: Post) => void;
|
|
273
|
+
deletePost: (id: string) => void;
|
|
274
|
+
selectPost: (id: string) => void;
|
|
275
|
+
setFilter: (filter: 'all' | 'favorites' | 'archived') => void;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export const usePostsStore = create<PostsState>((set) => ({
|
|
279
|
+
posts: [],
|
|
280
|
+
selectedPostId: null,
|
|
281
|
+
filter: 'all',
|
|
282
|
+
|
|
283
|
+
addPost: (post) => set((state) => ({ posts: [...state.posts, post] })),
|
|
284
|
+
deletePost: (id) =>
|
|
285
|
+
set((state) => ({
|
|
286
|
+
posts: state.posts.filter((p) => p.id !== id),
|
|
287
|
+
})),
|
|
288
|
+
selectPost: (id) => set({ selectedPostId: id }),
|
|
289
|
+
setFilter: (filter) => set({ filter }),
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Filter and map selectors
|
|
294
|
+
*/
|
|
295
|
+
export const useFavoritePosts = () => {
|
|
296
|
+
return usePostsStore((state) =>
|
|
297
|
+
state.posts.filter((post) => post.isFavorite)
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const useFilteredPosts = () => {
|
|
302
|
+
return usePostsStore((state) => {
|
|
303
|
+
switch (state.filter) {
|
|
304
|
+
case 'favorites':
|
|
305
|
+
return state.posts.filter((p) => p.isFavorite);
|
|
306
|
+
case 'archived':
|
|
307
|
+
return state.posts.filter((p) => p.archived);
|
|
308
|
+
default:
|
|
309
|
+
return state.posts;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Sorted selectors
|
|
316
|
+
*/
|
|
317
|
+
export const usePostsSortedByDate = () => {
|
|
318
|
+
return usePostsStore((state) =>
|
|
319
|
+
[...state.posts].sort(
|
|
320
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Count selectors
|
|
327
|
+
*/
|
|
328
|
+
export const usePostCount = () => {
|
|
329
|
+
return usePostsStore((state) => state.posts.length);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export const useFavoritePostCount = () => {
|
|
333
|
+
return usePostsStore((state) => state.posts.filter((p) => p.isFavorite).length);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Specific post selector
|
|
338
|
+
*/
|
|
339
|
+
export const usePostById = (postId: string) => {
|
|
340
|
+
return usePostsStore((state) =>
|
|
341
|
+
state.posts.find((p) => p.id === postId)
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export const useSelectedPost = () => {
|
|
346
|
+
return usePostsStore((state) => {
|
|
347
|
+
if (!state.selectedPostId) return null;
|
|
348
|
+
return state.posts.find((p) => p.id === state.selectedPostId) " null;
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Performance Optimization
|
|
354
|
+
|
|
355
|
+
### Equality Checking
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// File: src/hooks/useAuth.ts
|
|
359
|
+
import { useShallow } from 'zustand/react';
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Default: Shallow equality for objects
|
|
363
|
+
*/
|
|
364
|
+
function Component() {
|
|
365
|
+
// Without useShallow - object reference changes every time
|
|
366
|
+
const user = useAuthStore((state) => ({
|
|
367
|
+
id: state.user?.id,
|
|
368
|
+
email: state.user?.email,
|
|
369
|
+
})); // Re-renders if ANY store property changes
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* With useShallow - values are compared, not reference
|
|
374
|
+
*/
|
|
375
|
+
function Component() {
|
|
376
|
+
const user = useAuthStore(
|
|
377
|
+
useShallow((state) => ({
|
|
378
|
+
id: state.user?.id,
|
|
379
|
+
email: state.user?.email,
|
|
380
|
+
}))
|
|
381
|
+
); // Re-renders only if id or email changes
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Subscription Pattern
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
// File: src/utils/storeSubscription.ts
|
|
389
|
+
import { useEffect, useRef } from 'react';
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Subscribe to specific state changes
|
|
393
|
+
* Useful for side effects triggered by state changes
|
|
394
|
+
*/
|
|
395
|
+
export function useStoreSubscription<T>(
|
|
396
|
+
selector: (state: any) => T,
|
|
397
|
+
onChange: (value: T) => void
|
|
398
|
+
) {
|
|
399
|
+
const previousValue = useRef<T>();
|
|
400
|
+
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
const unsubscribe = useAuthStore.subscribe(
|
|
403
|
+
selector,
|
|
404
|
+
(value) => {
|
|
405
|
+
if (previousValue.current !== value) {
|
|
406
|
+
previousValue.current = value;
|
|
407
|
+
onChange(value);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
return unsubscribe;
|
|
413
|
+
}, [selector, onChange]);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Usage
|
|
417
|
+
function Component() {
|
|
418
|
+
useStoreSubscription(
|
|
419
|
+
(state) => state.isLoggedIn,
|
|
420
|
+
(isLoggedIn) => {
|
|
421
|
+
if (isLoggedIn) {
|
|
422
|
+
console.log('User logged in!');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Selector Best Practices
|
|
430
|
+
|
|
431
|
+
### ✅ DO
|
|
432
|
+
|
|
433
|
+
1. **Create specific selectors for each property**
|
|
434
|
+
```typescript
|
|
435
|
+
// ✅ RIGHT
|
|
436
|
+
export const useUserEmail = () => useAuthStore((s) => s.user?.email);
|
|
437
|
+
export const useIsLoggedIn = () => useAuthStore((s) => s.isLoggedIn);
|
|
438
|
+
|
|
439
|
+
function Component() {
|
|
440
|
+
const email = useUserEmail(); // Only re-renders if email changes
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
2. **Use selector hooks in components**
|
|
445
|
+
```typescript
|
|
446
|
+
// ✅ RIGHT
|
|
447
|
+
export const useUserData = () =>
|
|
448
|
+
useAuthStore(useShallow((s) => ({ user: s.user, profile: s.profile })));
|
|
449
|
+
|
|
450
|
+
function Component() {
|
|
451
|
+
const { user, profile } = useUserData();
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
3. **Memoize complex selectors**
|
|
456
|
+
```typescript
|
|
457
|
+
// ✅ RIGHT
|
|
458
|
+
export const useFilteredAndSorted = () => {
|
|
459
|
+
return usePostsStore((state) =>
|
|
460
|
+
state.posts
|
|
461
|
+
.filter((p) => p.published)
|
|
462
|
+
.sort((a, b) => b.date - a.date)
|
|
463
|
+
);
|
|
464
|
+
};
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### ❌ DON'T
|
|
468
|
+
|
|
469
|
+
1. **Don't destructure entire store in component**
|
|
470
|
+
```typescript
|
|
471
|
+
// ❌ WRONG - Re-renders on any store change
|
|
472
|
+
function Component() {
|
|
473
|
+
const { user, profile, loading, error, ... } = useAuthStore();
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
2. **Don't create selectors inline**
|
|
478
|
+
```typescript
|
|
479
|
+
// ❌ WRONG - Selector recreated every render
|
|
480
|
+
function Component() {
|
|
481
|
+
const email = useAuthStore(
|
|
482
|
+
(state) => state.user?.email // NEW function every render
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ✅ RIGHT - Extract to hook or useMemo
|
|
487
|
+
export const useUserEmail = () => useAuthStore((s) => s.user?.email);
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
3. **Don't perform expensive operations in selectors**
|
|
491
|
+
```typescript
|
|
492
|
+
// ❌ WRONG - Heavy computation on every store change
|
|
493
|
+
const processedData = useAuthStore((state) => {
|
|
494
|
+
return state.data.map(/* heavy processing */).sort().filter();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// ✅ RIGHT - Use useMemo or memoized selector
|
|
498
|
+
export const useProcessedData = () => {
|
|
499
|
+
return useAuthStore((state) => state.processedData); // Pre-computed
|
|
500
|
+
};
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Comparison: With vs Without Selectors
|
|
504
|
+
|
|
505
|
+
### Without Selectors (❌ Performance Issue)
|
|
506
|
+
```typescript
|
|
507
|
+
function UserCard() {
|
|
508
|
+
const { user, profile, isLoggedIn, loading, error } = useAuthStore();
|
|
509
|
+
// Re-renders when ANY property changes
|
|
510
|
+
return <Text>{user?.email}</Text>;
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### With Selectors (✅ Optimized)
|
|
515
|
+
```typescript
|
|
516
|
+
export const useUserEmail = () => useAuthStore((s) => s.user?.email);
|
|
517
|
+
|
|
518
|
+
function UserCard() {
|
|
519
|
+
const email = useUserEmail();
|
|
520
|
+
// Re-renders ONLY when email changes
|
|
521
|
+
return <Text>{email}</Text>;
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Performance Impact:**
|
|
526
|
+
- Without selectors: UserCard re-renders when loading, error, profile change (unnecessary)
|
|
527
|
+
- With selectors: UserCard re-renders only when email changes (efficient)
|
|
528
|
+
|
|
529
|
+
## Related Patterns
|
|
530
|
+
|
|
531
|
+
- [Store Organization](./store-organization.md) — Multiple focused stores
|
|
532
|
+
- [Persistence Middleware](./persistence-middleware.md) — State persistence
|
|
533
|
+
- [Zustand Patterns](./zustand-patterns.md) — Zustand fundamentals
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
*Pattern extracted from production repositories: core-monorepo, PokePages, DJsPortfolio*
|
|
538
|
+
*Files: src/hooks/ directory, src/store/ with custom selectors*
|