@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,521 @@
|
|
|
1
|
+
# Persistence Middleware Pattern
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Persistence middleware enables Zustand stores to automatically save state to AsyncStorage (mobile) or localStorage (web), with automatic hydration on app startup. This pattern provides offline-first state management where user data persists across app sessions and devices.
|
|
6
|
+
|
|
7
|
+
## When to Use
|
|
8
|
+
|
|
9
|
+
**Use persistence middleware** for:
|
|
10
|
+
- ✅ User authentication (sessions, tokens, profiles)
|
|
11
|
+
- ✅ User preferences and settings
|
|
12
|
+
- ✅ Feature state (favorites, tracking, progress)
|
|
13
|
+
- ✅ Feature flags and configuration
|
|
14
|
+
- ✅ Offline-first user data
|
|
15
|
+
- ✅ Quick app resumption (instant state restoration)
|
|
16
|
+
|
|
17
|
+
**Don't persist:**
|
|
18
|
+
- ❌ Transient loading/error states
|
|
19
|
+
- ❌ Real-time data (scores, counts from API)
|
|
20
|
+
- ❌ Sensitive data (without encryption)
|
|
21
|
+
- ❌ Temporary UI state (modal visibility)
|
|
22
|
+
|
|
23
|
+
## Core Concepts
|
|
24
|
+
|
|
25
|
+
**Persist middleware workflow:**
|
|
26
|
+
1. **Initialize**: App loads, hydrate stored state from storage
|
|
27
|
+
2. **Use**: Normal store operations, state updates automatically
|
|
28
|
+
3. **Save**: Middleware detects changes, writes to storage
|
|
29
|
+
4. **Restore**: User reopens app, stored state automatically loaded
|
|
30
|
+
|
|
31
|
+
**Storage options:**
|
|
32
|
+
- Mobile (React Native): `@react-native-async-storage/async-storage`
|
|
33
|
+
- Web: Built-in `localStorage`
|
|
34
|
+
- Cross-platform: Conditional import via platform detection
|
|
35
|
+
|
|
36
|
+
## Code Example
|
|
37
|
+
|
|
38
|
+
### Basic Persistence Setup
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// File: src/store/authStore.ts
|
|
42
|
+
import { create } from 'zustand';
|
|
43
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
44
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
45
|
+
|
|
46
|
+
interface AuthState {
|
|
47
|
+
user: User | null;
|
|
48
|
+
profile: Profile | null;
|
|
49
|
+
session: Session | null;
|
|
50
|
+
isLoggedIn: boolean;
|
|
51
|
+
loading: boolean;
|
|
52
|
+
error: string | null;
|
|
53
|
+
|
|
54
|
+
setUser: (user: User | null) => void;
|
|
55
|
+
signOut: () => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const useAuthStore = create<AuthState>()(
|
|
59
|
+
persist(
|
|
60
|
+
(set, get) => ({
|
|
61
|
+
// State and actions
|
|
62
|
+
user: null,
|
|
63
|
+
profile: null,
|
|
64
|
+
session: null,
|
|
65
|
+
isLoggedIn: false,
|
|
66
|
+
loading: true,
|
|
67
|
+
error: null,
|
|
68
|
+
|
|
69
|
+
setUser: (user) => set({ user, isLoggedIn: !!user }),
|
|
70
|
+
|
|
71
|
+
signOut: async () => {
|
|
72
|
+
await supabase.auth.signOut();
|
|
73
|
+
set({
|
|
74
|
+
user: null,
|
|
75
|
+
profile: null,
|
|
76
|
+
session: null,
|
|
77
|
+
isLoggedIn: false,
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
{
|
|
82
|
+
// Persistence configuration
|
|
83
|
+
name: 'auth-storage', // Storage key
|
|
84
|
+
storage: createJSONStorage(() => AsyncStorage), // Storage adapter
|
|
85
|
+
partialize: (state) => ({ // Partial persistence
|
|
86
|
+
user: state.user,
|
|
87
|
+
profile: state.profile,
|
|
88
|
+
session: state.session,
|
|
89
|
+
// NOT persisted: loading, error (transient)
|
|
90
|
+
}),
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Cross-Platform Storage
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// File: src/utils/storage.ts
|
|
100
|
+
import { Platform } from 'react-native';
|
|
101
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
102
|
+
import { StorageValue } from 'zustand/middleware';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Cross-platform storage adapter
|
|
106
|
+
* Mobile: AsyncStorage
|
|
107
|
+
* Web: localStorage
|
|
108
|
+
*/
|
|
109
|
+
export const createCrossPlatformStorage = () => {
|
|
110
|
+
if (Platform.OS === 'web') {
|
|
111
|
+
return {
|
|
112
|
+
getItem: (name: string): StorageValue | null => {
|
|
113
|
+
const value = localStorage.getItem(name);
|
|
114
|
+
return value ? JSON.parse(value) : null;
|
|
115
|
+
},
|
|
116
|
+
setItem: (name: string, value: StorageValue): void => {
|
|
117
|
+
localStorage.setItem(name, JSON.stringify(value));
|
|
118
|
+
},
|
|
119
|
+
removeItem: (name: string): void => {
|
|
120
|
+
localStorage.removeItem(name);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
getItem: async (name: string): Promise<StorageValue | null> => {
|
|
127
|
+
const value = await AsyncStorage.getItem(name);
|
|
128
|
+
return value ? JSON.parse(value) : null;
|
|
129
|
+
},
|
|
130
|
+
setItem: async (name: string, value: StorageValue): Promise<void> => {
|
|
131
|
+
await AsyncStorage.setItem(name, JSON.stringify(value));
|
|
132
|
+
},
|
|
133
|
+
removeItem: async (name: string): Promise<void> => {
|
|
134
|
+
await AsyncStorage.removeItem(name);
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Usage
|
|
140
|
+
const storage = createCrossPlatformStorage();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Partial Persistence Pattern
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// File: src/store/settingsStore.ts
|
|
147
|
+
import { create } from 'zustand';
|
|
148
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
149
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
150
|
+
|
|
151
|
+
interface SettingsState {
|
|
152
|
+
// User preferences (PERSIST these)
|
|
153
|
+
theme: 'light' | 'dark' | 'auto';
|
|
154
|
+
language: string;
|
|
155
|
+
notifications: boolean;
|
|
156
|
+
compactMode: boolean;
|
|
157
|
+
|
|
158
|
+
// Transient UI state (DON'T persist)
|
|
159
|
+
loading: boolean;
|
|
160
|
+
error: string | null;
|
|
161
|
+
syncInProgress: boolean;
|
|
162
|
+
|
|
163
|
+
// Actions
|
|
164
|
+
setTheme: (theme: 'light' | 'dark' | 'auto') => void;
|
|
165
|
+
setLanguage: (language: string) => void;
|
|
166
|
+
toggleNotifications: () => void;
|
|
167
|
+
setSyncInProgress: (inProgress: boolean) => void;
|
|
168
|
+
resetSettings: () => void;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const initialState: Pick<
|
|
172
|
+
SettingsState,
|
|
173
|
+
'theme' | 'language' | 'notifications' | 'compactMode'
|
|
174
|
+
> = {
|
|
175
|
+
theme: 'auto',
|
|
176
|
+
language: 'en',
|
|
177
|
+
notifications: true,
|
|
178
|
+
compactMode: false,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export const useSettingsStore = create<SettingsState>()(
|
|
182
|
+
persist(
|
|
183
|
+
(set) => ({
|
|
184
|
+
...initialState,
|
|
185
|
+
|
|
186
|
+
// Transient state
|
|
187
|
+
loading: false,
|
|
188
|
+
error: null,
|
|
189
|
+
syncInProgress: false,
|
|
190
|
+
|
|
191
|
+
// Actions
|
|
192
|
+
setTheme: (theme) => set({ theme }),
|
|
193
|
+
setLanguage: (language) => set({ language }),
|
|
194
|
+
toggleNotifications: () =>
|
|
195
|
+
set((state) => ({ notifications: !state.notifications })),
|
|
196
|
+
setSyncInProgress: (inProgress) => set({ syncInProgress: inProgress }),
|
|
197
|
+
|
|
198
|
+
resetSettings: () => set(initialState),
|
|
199
|
+
}),
|
|
200
|
+
{
|
|
201
|
+
name: 'settings-storage',
|
|
202
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
203
|
+
|
|
204
|
+
// Key pattern: Only persist user preferences, not transient state
|
|
205
|
+
partialize: (state) => ({
|
|
206
|
+
theme: state.theme,
|
|
207
|
+
language: state.language,
|
|
208
|
+
notifications: state.notifications,
|
|
209
|
+
compactMode: state.compactMode,
|
|
210
|
+
// Excluded: loading, error, syncInProgress
|
|
211
|
+
}),
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Middleware Composition
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// File: src/store/appStore.ts
|
|
221
|
+
import { create } from 'zustand';
|
|
222
|
+
import { persist, createJSONStorage, devtools } from 'zustand/middleware';
|
|
223
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
224
|
+
|
|
225
|
+
interface AppState {
|
|
226
|
+
appVersion: string;
|
|
227
|
+
lastOpenedAt: number;
|
|
228
|
+
userPreferences: Record<string, any>;
|
|
229
|
+
|
|
230
|
+
setAppVersion: (version: string) => void;
|
|
231
|
+
updatePreference: (key: string, value: any) => void;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const useAppStore = create<AppState>()(
|
|
235
|
+
devtools( // Dev tools wrapper (outer)
|
|
236
|
+
persist( // Persistence middleware (middle)
|
|
237
|
+
(set) => ({
|
|
238
|
+
appVersion: '1.0.0',
|
|
239
|
+
lastOpenedAt: Date.now(),
|
|
240
|
+
userPreferences: {},
|
|
241
|
+
|
|
242
|
+
setAppVersion: (version) => set({ appVersion: version }),
|
|
243
|
+
|
|
244
|
+
updatePreference: (key, value) =>
|
|
245
|
+
set((state) => ({
|
|
246
|
+
userPreferences: {
|
|
247
|
+
...state.userPreferences,
|
|
248
|
+
[key]: value,
|
|
249
|
+
},
|
|
250
|
+
})),
|
|
251
|
+
}),
|
|
252
|
+
{
|
|
253
|
+
name: 'app-storage',
|
|
254
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
255
|
+
}
|
|
256
|
+
),
|
|
257
|
+
{ name: 'AppStore' }
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### State Hydration & Initialization
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// File: src/hooks/useAppInitialization.ts
|
|
266
|
+
import { useEffect, useState } from 'react';
|
|
267
|
+
import { useAuthStore } from '@/store/authStore';
|
|
268
|
+
import { useDexTrackerStore } from '@/store/dexTrackerStore';
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Initialize app state on startup
|
|
272
|
+
* Hydrates persisted stores and performs async initialization
|
|
273
|
+
*/
|
|
274
|
+
export function useAppInitialization() {
|
|
275
|
+
const [isReady, setIsReady] = useState(false);
|
|
276
|
+
const authStore = useAuthStore();
|
|
277
|
+
const dexStore = useDexTrackerStore();
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
const initializeApp = async () => {
|
|
281
|
+
try {
|
|
282
|
+
// Stores auto-hydrate from storage (middleware)
|
|
283
|
+
// Do any async initialization after hydration
|
|
284
|
+
if (authStore.isLoggedIn && authStore.user) {
|
|
285
|
+
// Verify session is still valid
|
|
286
|
+
// Sync user data from API if needed
|
|
287
|
+
await authStore.initializeAuth();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Other initialization tasks
|
|
291
|
+
// ...
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error('App initialization error:', error);
|
|
294
|
+
} finally {
|
|
295
|
+
setIsReady(true);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
initializeApp();
|
|
300
|
+
}, []);
|
|
301
|
+
|
|
302
|
+
return { isReady };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Usage in root component
|
|
306
|
+
export function App() {
|
|
307
|
+
const { isReady } = useAppInitialization();
|
|
308
|
+
|
|
309
|
+
if (!isReady) {
|
|
310
|
+
return <SplashScreen />;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return <RootLayout />;
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Storage Migration Pattern
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// File: src/utils/storageMigration.ts
|
|
321
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Migrate persisted state when schema changes
|
|
325
|
+
* Called before store initialization
|
|
326
|
+
*/
|
|
327
|
+
export async function migrateStorage() {
|
|
328
|
+
try {
|
|
329
|
+
const authData = await AsyncStorage.getItem('auth-storage');
|
|
330
|
+
if (!authData) return;
|
|
331
|
+
|
|
332
|
+
const state = JSON.parse(authData);
|
|
333
|
+
|
|
334
|
+
// Migrate from v1 to v2
|
|
335
|
+
if (state.version === 1) {
|
|
336
|
+
// Transform old structure to new structure
|
|
337
|
+
const migratedState = {
|
|
338
|
+
...state,
|
|
339
|
+
version: 2,
|
|
340
|
+
// Add new fields with defaults
|
|
341
|
+
profile: state.profile || null,
|
|
342
|
+
// Remove deprecated fields
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
delete migratedState.oldField;
|
|
346
|
+
|
|
347
|
+
await AsyncStorage.setItem('auth-storage', JSON.stringify(migratedState));
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error('Storage migration error:', error);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Call during app initialization
|
|
355
|
+
export function App() {
|
|
356
|
+
useEffect(() => {
|
|
357
|
+
migrateStorage().then(() => {
|
|
358
|
+
// Initialize stores
|
|
359
|
+
});
|
|
360
|
+
}, []);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Error Handling & Recovery
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// File: src/store/withPersistenceError.ts
|
|
368
|
+
import { create } from 'zustand';
|
|
369
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
370
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
371
|
+
|
|
372
|
+
interface SafeStorageState {
|
|
373
|
+
data: any;
|
|
374
|
+
storageError: string | null;
|
|
375
|
+
clearStorage: () => Promise<void>;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Storage adapter with error handling and recovery
|
|
380
|
+
*/
|
|
381
|
+
const safeStorage = createJSONStorage(() => AsyncStorage);
|
|
382
|
+
|
|
383
|
+
const originalGetItem = safeStorage.getItem;
|
|
384
|
+
const originalSetItem = safeStorage.setItem;
|
|
385
|
+
|
|
386
|
+
const SafeJSONStorage = {
|
|
387
|
+
...safeStorage,
|
|
388
|
+
|
|
389
|
+
getItem: async (name: string) => {
|
|
390
|
+
try {
|
|
391
|
+
return await originalGetItem(name);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error(`Error reading from storage (${name}):`, error);
|
|
394
|
+
// Return null on error - store will use defaults
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
setItem: async (name: string, value: any) => {
|
|
400
|
+
try {
|
|
401
|
+
await originalSetItem(name, value);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error(`Error writing to storage (${name}):`, error);
|
|
404
|
+
// Continue - store remains in memory even if storage fails
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
removeItem: async (name: string) => {
|
|
409
|
+
try {
|
|
410
|
+
await safeStorage.removeItem(name);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error(`Error removing storage (${name}):`, error);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Use in store
|
|
418
|
+
export const useSafeStore = create<SafeStorageState>()(
|
|
419
|
+
persist(
|
|
420
|
+
(set) => ({
|
|
421
|
+
data: null,
|
|
422
|
+
storageError: null,
|
|
423
|
+
|
|
424
|
+
clearStorage: async () => {
|
|
425
|
+
try {
|
|
426
|
+
await AsyncStorage.removeItem('safe-storage');
|
|
427
|
+
set({ data: null });
|
|
428
|
+
} catch (error) {
|
|
429
|
+
set({
|
|
430
|
+
storageError: error instanceof Error ? error.message : 'Unknown error',
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
}),
|
|
435
|
+
{
|
|
436
|
+
name: 'safe-storage',
|
|
437
|
+
storage: SafeJSONStorage,
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Persistence Best Practices
|
|
444
|
+
|
|
445
|
+
### ✅ DO
|
|
446
|
+
|
|
447
|
+
1. **Partition persistent vs transient state**
|
|
448
|
+
```typescript
|
|
449
|
+
partialize: (state) => ({
|
|
450
|
+
user: state.user, // ✅ Persist
|
|
451
|
+
preferences: state.preferences,
|
|
452
|
+
isLoggedIn: state.isLoggedIn,
|
|
453
|
+
// NOT included: loading, error (transient)
|
|
454
|
+
})
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
2. **Use reasonable storage keys**
|
|
458
|
+
```typescript
|
|
459
|
+
{
|
|
460
|
+
name: 'auth-storage', // ✅ Descriptive
|
|
461
|
+
// NOT 'store1', 'store-2', etc.
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
3. **Handle storage errors gracefully**
|
|
466
|
+
```typescript
|
|
467
|
+
try {
|
|
468
|
+
await AsyncStorage.setItem(key, value);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
// Log but continue - state still in memory
|
|
471
|
+
console.error('Storage write failed:', error);
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### ❌ DON'T
|
|
476
|
+
|
|
477
|
+
1. **Don't persist sensitive data without encryption**
|
|
478
|
+
```typescript
|
|
479
|
+
// ❌ WRONG
|
|
480
|
+
partialize: (state) => ({
|
|
481
|
+
apiKey: state.apiKey, // Never persist secrets!
|
|
482
|
+
password: state.password,
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// ✅ RIGHT
|
|
486
|
+
// Store sensitive data in secure storage (expo-secure-store, etc.)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
2. **Don't create too many storage keys**
|
|
490
|
+
```typescript
|
|
491
|
+
// ❌ WRONG - Storage bloat
|
|
492
|
+
persist((set) => ({...}), { name: 'store1' })
|
|
493
|
+
persist((set) => ({...}), { name: 'store2' })
|
|
494
|
+
persist((set) => ({...}), { name: 'store3' })
|
|
495
|
+
|
|
496
|
+
// ✅ RIGHT - Organized stores with clear names
|
|
497
|
+
persist((set) => ({...}), { name: 'auth-storage' })
|
|
498
|
+
persist((set) => ({...}), { name: 'settings-storage' })
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
3. **Don't forget migration strategy**
|
|
502
|
+
```typescript
|
|
503
|
+
// ✅ Always include version field
|
|
504
|
+
partialize: (state) => ({
|
|
505
|
+
version: 1,
|
|
506
|
+
user: state.user,
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// Then check version on init and migrate if needed
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Related Patterns
|
|
513
|
+
|
|
514
|
+
- [Store Organization](./store-organization.md) — Multi-store architecture
|
|
515
|
+
- [Selector Hooks](./selector-hooks.md) — Performance optimization
|
|
516
|
+
- [Zustand Patterns](./zustand-patterns.md) — Zustand fundamentals
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
*Pattern extracted from production repositories: core-monorepo, PokePages, DJsPortfolio*
|
|
521
|
+
*Files: src/store/ directory with persist middleware*
|