@croacroa/react-native-template 1.0.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.
Files changed (109) hide show
  1. package/.env.example +18 -0
  2. package/.eslintrc.js +55 -0
  3. package/.github/workflows/ci.yml +184 -0
  4. package/.github/workflows/eas-build.yml +55 -0
  5. package/.github/workflows/eas-update.yml +50 -0
  6. package/.gitignore +62 -0
  7. package/.prettierrc +11 -0
  8. package/.storybook/main.ts +28 -0
  9. package/.storybook/preview.tsx +30 -0
  10. package/CHANGELOG.md +106 -0
  11. package/CONTRIBUTING.md +377 -0
  12. package/README.md +399 -0
  13. package/__tests__/components/Button.test.tsx +74 -0
  14. package/__tests__/hooks/useAuth.test.tsx +499 -0
  15. package/__tests__/services/api.test.ts +535 -0
  16. package/__tests__/utils/cn.test.ts +39 -0
  17. package/app/(auth)/_layout.tsx +36 -0
  18. package/app/(auth)/home.tsx +117 -0
  19. package/app/(auth)/profile.tsx +152 -0
  20. package/app/(auth)/settings.tsx +147 -0
  21. package/app/(public)/_layout.tsx +21 -0
  22. package/app/(public)/forgot-password.tsx +127 -0
  23. package/app/(public)/login.tsx +120 -0
  24. package/app/(public)/onboarding.tsx +5 -0
  25. package/app/(public)/register.tsx +139 -0
  26. package/app/_layout.tsx +97 -0
  27. package/app/index.tsx +21 -0
  28. package/app.config.ts +72 -0
  29. package/assets/images/.gitkeep +7 -0
  30. package/assets/images/adaptive-icon.png +0 -0
  31. package/assets/images/favicon.png +0 -0
  32. package/assets/images/icon.png +0 -0
  33. package/assets/images/notification-icon.png +0 -0
  34. package/assets/images/splash.png +0 -0
  35. package/babel.config.js +10 -0
  36. package/components/ErrorBoundary.tsx +169 -0
  37. package/components/forms/FormInput.tsx +78 -0
  38. package/components/forms/index.ts +1 -0
  39. package/components/onboarding/OnboardingScreen.tsx +370 -0
  40. package/components/onboarding/index.ts +2 -0
  41. package/components/ui/AnimatedButton.tsx +156 -0
  42. package/components/ui/AnimatedCard.tsx +108 -0
  43. package/components/ui/Avatar.tsx +316 -0
  44. package/components/ui/Badge.tsx +416 -0
  45. package/components/ui/BottomSheet.tsx +307 -0
  46. package/components/ui/Button.stories.tsx +115 -0
  47. package/components/ui/Button.tsx +104 -0
  48. package/components/ui/Card.stories.tsx +84 -0
  49. package/components/ui/Card.tsx +32 -0
  50. package/components/ui/Checkbox.tsx +261 -0
  51. package/components/ui/Input.stories.tsx +106 -0
  52. package/components/ui/Input.tsx +117 -0
  53. package/components/ui/Modal.tsx +98 -0
  54. package/components/ui/OptimizedImage.tsx +369 -0
  55. package/components/ui/Select.tsx +240 -0
  56. package/components/ui/Skeleton.tsx +180 -0
  57. package/components/ui/index.ts +18 -0
  58. package/constants/config.ts +54 -0
  59. package/docs/adr/001-state-management.md +79 -0
  60. package/docs/adr/002-styling-approach.md +130 -0
  61. package/docs/adr/003-data-fetching.md +155 -0
  62. package/docs/adr/004-auth-adapter-pattern.md +144 -0
  63. package/docs/adr/README.md +78 -0
  64. package/eas.json +47 -0
  65. package/global.css +10 -0
  66. package/hooks/index.ts +25 -0
  67. package/hooks/useApi.ts +236 -0
  68. package/hooks/useAuth.tsx +290 -0
  69. package/hooks/useBiometrics.ts +295 -0
  70. package/hooks/useDeepLinking.ts +256 -0
  71. package/hooks/useNotifications.ts +138 -0
  72. package/hooks/useOffline.ts +69 -0
  73. package/hooks/usePerformance.ts +434 -0
  74. package/hooks/useTheme.tsx +85 -0
  75. package/hooks/useUpdates.ts +358 -0
  76. package/i18n/index.ts +77 -0
  77. package/i18n/locales/en.json +101 -0
  78. package/i18n/locales/fr.json +101 -0
  79. package/jest.config.js +32 -0
  80. package/maestro/README.md +113 -0
  81. package/maestro/config.yaml +35 -0
  82. package/maestro/flows/login.yaml +62 -0
  83. package/maestro/flows/navigation.yaml +68 -0
  84. package/maestro/flows/offline.yaml +60 -0
  85. package/maestro/flows/register.yaml +94 -0
  86. package/metro.config.js +6 -0
  87. package/nativewind-env.d.ts +1 -0
  88. package/package.json +170 -0
  89. package/scripts/init.ps1 +162 -0
  90. package/scripts/init.sh +174 -0
  91. package/services/analytics.ts +428 -0
  92. package/services/api.ts +340 -0
  93. package/services/authAdapter.ts +333 -0
  94. package/services/index.ts +22 -0
  95. package/services/queryClient.ts +97 -0
  96. package/services/sentry.ts +131 -0
  97. package/services/storage.ts +82 -0
  98. package/stores/appStore.ts +54 -0
  99. package/stores/index.ts +2 -0
  100. package/stores/notificationStore.ts +40 -0
  101. package/tailwind.config.js +47 -0
  102. package/tsconfig.json +26 -0
  103. package/types/index.ts +42 -0
  104. package/types/user.ts +63 -0
  105. package/utils/accessibility.ts +446 -0
  106. package/utils/cn.ts +14 -0
  107. package/utils/index.ts +43 -0
  108. package/utils/toast.ts +113 -0
  109. package/utils/validation.ts +67 -0
@@ -0,0 +1,180 @@
1
+ import { useEffect } from "react";
2
+ import { View, ViewProps } from "react-native";
3
+ import Animated, {
4
+ useSharedValue,
5
+ useAnimatedStyle,
6
+ withRepeat,
7
+ withTiming,
8
+ interpolate,
9
+ } from "react-native-reanimated";
10
+ import { cn } from "@/utils/cn";
11
+ import { useTheme } from "@/hooks/useTheme";
12
+
13
+ interface SkeletonProps extends ViewProps {
14
+ width?: number | string;
15
+ height?: number | string;
16
+ borderRadius?: number;
17
+ className?: string;
18
+ }
19
+
20
+ /**
21
+ * Animated skeleton loader component
22
+ * Features shimmer effect that works in both light and dark mode
23
+ */
24
+ export function Skeleton({
25
+ width = "100%",
26
+ height = 20,
27
+ borderRadius = 8,
28
+ className,
29
+ style,
30
+ ...props
31
+ }: SkeletonProps) {
32
+ const { isDark } = useTheme();
33
+ const shimmer = useSharedValue(0);
34
+
35
+ useEffect(() => {
36
+ shimmer.value = withRepeat(
37
+ withTiming(1, { duration: 1500 }),
38
+ -1, // infinite
39
+ false // no reverse
40
+ );
41
+ }, []);
42
+
43
+ const animatedStyle = useAnimatedStyle(() => {
44
+ const opacity = interpolate(shimmer.value, [0, 0.5, 1], [0.3, 0.6, 0.3]);
45
+ return { opacity };
46
+ });
47
+
48
+ return (
49
+ <Animated.View
50
+ style={[
51
+ {
52
+ width: width as number | `${number}%`,
53
+ height: height as number | `${number}%`,
54
+ borderRadius,
55
+ backgroundColor: isDark ? "#334155" : "#e2e8f0",
56
+ },
57
+ animatedStyle,
58
+ style,
59
+ ]}
60
+ className={cn(className)}
61
+ {...props}
62
+ />
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Text skeleton - for single line text
68
+ */
69
+ export function SkeletonText({
70
+ width = "100%",
71
+ height = 16,
72
+ className,
73
+ ...props
74
+ }: SkeletonProps) {
75
+ return (
76
+ <Skeleton
77
+ width={width}
78
+ height={height}
79
+ borderRadius={4}
80
+ className={className}
81
+ {...props}
82
+ />
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Circle skeleton - for avatars
88
+ */
89
+ export function SkeletonCircle({
90
+ size = 48,
91
+ className,
92
+ ...props
93
+ }: Omit<SkeletonProps, "width" | "height" | "borderRadius"> & { size?: number }) {
94
+ return (
95
+ <Skeleton
96
+ width={size}
97
+ height={size}
98
+ borderRadius={size / 2}
99
+ className={className}
100
+ {...props}
101
+ />
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Card skeleton - common pattern for list items
107
+ */
108
+ export function SkeletonCard({ className }: { className?: string }) {
109
+ return (
110
+ <View
111
+ className={cn(
112
+ "rounded-xl bg-surface-light p-4 dark:bg-surface-dark",
113
+ className
114
+ )}
115
+ >
116
+ <View className="flex-row items-center gap-3">
117
+ <SkeletonCircle size={48} />
118
+ <View className="flex-1 gap-2">
119
+ <SkeletonText width="60%" height={14} />
120
+ <SkeletonText width="40%" height={12} />
121
+ </View>
122
+ </View>
123
+ </View>
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Profile skeleton - for profile screens
129
+ */
130
+ export function SkeletonProfile({ className }: { className?: string }) {
131
+ return (
132
+ <View className={cn("items-center gap-4", className)}>
133
+ <SkeletonCircle size={96} />
134
+ <SkeletonText width={150} height={24} />
135
+ <SkeletonText width={200} height={14} />
136
+ </View>
137
+ );
138
+ }
139
+
140
+ /**
141
+ * List skeleton - for list views
142
+ */
143
+ export function SkeletonList({
144
+ count = 5,
145
+ className,
146
+ }: {
147
+ count?: number;
148
+ className?: string;
149
+ }) {
150
+ return (
151
+ <View className={cn("gap-3", className)}>
152
+ {Array.from({ length: count }).map((_, index) => (
153
+ <SkeletonCard key={index} />
154
+ ))}
155
+ </View>
156
+ );
157
+ }
158
+
159
+ /**
160
+ * Form skeleton - for form screens
161
+ */
162
+ export function SkeletonForm({ className }: { className?: string }) {
163
+ return (
164
+ <View className={cn("gap-4", className)}>
165
+ <View className="gap-2">
166
+ <SkeletonText width={60} height={12} />
167
+ <Skeleton height={48} />
168
+ </View>
169
+ <View className="gap-2">
170
+ <SkeletonText width={80} height={12} />
171
+ <Skeleton height={48} />
172
+ </View>
173
+ <View className="gap-2">
174
+ <SkeletonText width={70} height={12} />
175
+ <Skeleton height={48} />
176
+ </View>
177
+ <Skeleton height={48} className="mt-4" />
178
+ </View>
179
+ );
180
+ }
@@ -0,0 +1,18 @@
1
+ export { Button } from "./Button";
2
+ export { Input } from "./Input";
3
+ export { Card } from "./Card";
4
+ export { Modal } from "./Modal";
5
+ export { AnimatedButton } from "./AnimatedButton";
6
+ export { AnimatedCard, AnimatedList } from "./AnimatedCard";
7
+ export { Select } from "./Select";
8
+ export type { SelectOption } from "./Select";
9
+ export { Checkbox, CheckboxGroup } from "./Checkbox";
10
+ export { BottomSheet, useBottomSheet } from "./BottomSheet";
11
+ export type { BottomSheetRef } from "./BottomSheet";
12
+ export { Avatar, AvatarGroup } from "./Avatar";
13
+ export { Badge, Chip, CountBadge } from "./Badge";
14
+ export {
15
+ OptimizedImage,
16
+ BackgroundImage,
17
+ ProgressiveImage,
18
+ } from "./OptimizedImage";
@@ -0,0 +1,54 @@
1
+ import Constants from "expo-constants";
2
+
3
+ // Environment detection
4
+ export const IS_DEV = __DEV__;
5
+ export const IS_PREVIEW =
6
+ Constants.expoConfig?.extra?.APP_VARIANT === "preview";
7
+ export const IS_PROD = !IS_DEV && !IS_PREVIEW;
8
+
9
+ // API Configuration
10
+ // TODO: Replace with your actual API URLs
11
+ export const API_URL = IS_PROD
12
+ ? "https://api.yourapp.com"
13
+ : IS_PREVIEW
14
+ ? "https://staging-api.yourapp.com"
15
+ : "http://localhost:3000";
16
+
17
+ // App Configuration
18
+ export const APP_NAME = Constants.expoConfig?.name || "YourApp";
19
+ export const APP_VERSION = Constants.expoConfig?.version || "1.0.0";
20
+ export const APP_SCHEME = Constants.expoConfig?.scheme || "yourapp";
21
+
22
+ // Feature Flags
23
+ export const FEATURES = {
24
+ ENABLE_ANALYTICS: IS_PROD,
25
+ ENABLE_CRASH_REPORTING: IS_PROD,
26
+ ENABLE_PUSH_NOTIFICATIONS: true,
27
+ ENABLE_BIOMETRIC_AUTH: true,
28
+ ENABLE_PERFORMANCE_MONITORING: IS_DEV || IS_PREVIEW,
29
+ } as const;
30
+
31
+ // Export individual flags for convenience
32
+ export const ENABLE_ANALYTICS = FEATURES.ENABLE_ANALYTICS;
33
+ export const ENABLE_CRASH_REPORTING = FEATURES.ENABLE_CRASH_REPORTING;
34
+ export const ENABLE_PUSH_NOTIFICATIONS = FEATURES.ENABLE_PUSH_NOTIFICATIONS;
35
+ export const ENABLE_BIOMETRIC_AUTH = FEATURES.ENABLE_BIOMETRIC_AUTH;
36
+ export const ENABLE_PERFORMANCE_MONITORING =
37
+ FEATURES.ENABLE_PERFORMANCE_MONITORING;
38
+
39
+ // Timing Constants
40
+ export const TIMING = {
41
+ DEBOUNCE_MS: 300,
42
+ ANIMATION_DURATION_MS: 200,
43
+ TOAST_DURATION_MS: 3000,
44
+ API_TIMEOUT_MS: 30000,
45
+ } as const;
46
+
47
+ // Storage Keys
48
+ export const STORAGE_KEYS = {
49
+ AUTH_TOKEN: "auth_token",
50
+ USER: "auth_user",
51
+ THEME: "theme_mode",
52
+ ONBOARDING_COMPLETED: "onboarding_completed",
53
+ PUSH_TOKEN: "push_token",
54
+ } as const;
@@ -0,0 +1,79 @@
1
+ # ADR-001: Use Zustand for State Management
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Date
8
+
9
+ 2024-01-01
10
+
11
+ ## Context
12
+
13
+ We need a state management solution for our React Native application. The requirements are:
14
+
15
+ 1. Simple API with minimal boilerplate
16
+ 2. Good TypeScript support
17
+ 3. Small bundle size (mobile performance matters)
18
+ 4. Support for persistence
19
+ 5. Devtools support for debugging
20
+ 6. No need for complex middleware or actions
21
+
22
+ Options considered:
23
+
24
+ - **Redux Toolkit**: Industry standard, but verbose for our needs
25
+ - **MobX**: Powerful but adds complexity with observables
26
+ - **Jotai**: Atomic state, good for simple cases
27
+ - **Zustand**: Simple, small, flexible
28
+ - **React Context**: Built-in, but can cause performance issues
29
+
30
+ ## Decision
31
+
32
+ We chose **Zustand** for global state management.
33
+
34
+ ## Rationale
35
+
36
+ 1. **Simplicity**: Creating a store is just a function call
37
+
38
+ ```ts
39
+ const useStore = create((set) => ({
40
+ count: 0,
41
+ increment: () => set((s) => ({ count: s.count + 1 })),
42
+ }));
43
+ ```
44
+
45
+ 2. **Bundle size**: ~1KB gzipped vs Redux's ~7KB
46
+
47
+ 3. **TypeScript**: Excellent inference, minimal type annotations needed
48
+
49
+ 4. **Persistence**: Easy integration with AsyncStorage via middleware
50
+
51
+ 5. **No Providers**: Works outside React components, useful for API services
52
+
53
+ 6. **Selective subscriptions**: Components only re-render when selected state changes
54
+
55
+ ## Consequences
56
+
57
+ ### Positive
58
+
59
+ - Faster development with less boilerplate
60
+ - Smaller bundle size
61
+ - Easy to test stores
62
+ - Can use stores outside React (e.g., in API handlers)
63
+
64
+ ### Negative
65
+
66
+ - Less structured than Redux (could lead to inconsistent patterns)
67
+ - Smaller ecosystem than Redux
68
+ - Team needs to establish conventions
69
+
70
+ ### Mitigation
71
+
72
+ - Document store patterns in CONTRIBUTING.md
73
+ - Use TypeScript for store definitions
74
+ - Keep stores focused (one concern per store)
75
+
76
+ ## References
77
+
78
+ - [Zustand Documentation](https://github.com/pmndrs/zustand)
79
+ - [React Native Performance](https://reactnative.dev/docs/performance)
@@ -0,0 +1,130 @@
1
+ # ADR-002: Use NativeWind (Tailwind CSS) for Styling
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Date
8
+
9
+ 2024-01-01
10
+
11
+ ## Context
12
+
13
+ We need a consistent styling approach for our React Native application. The requirements are:
14
+
15
+ 1. Developer productivity (fast to write styles)
16
+ 2. Consistent design system
17
+ 3. Dark mode support
18
+ 4. Good performance
19
+ 5. Familiar to web developers
20
+
21
+ Options considered:
22
+
23
+ - **StyleSheet.create**: React Native's built-in solution
24
+ - **Styled Components**: CSS-in-JS, popular in web
25
+ - **NativeWind**: Tailwind CSS for React Native
26
+ - **Tamagui**: Universal design system
27
+ - **Dripsy**: Responsive design system
28
+
29
+ ## Decision
30
+
31
+ We chose **NativeWind** (Tailwind CSS for React Native).
32
+
33
+ ## Rationale
34
+
35
+ 1. **Developer Experience**: Utility classes are fast to write
36
+
37
+ ```tsx
38
+ // NativeWind
39
+ <View className="p-4 bg-white rounded-xl shadow-lg">
40
+
41
+ // StyleSheet
42
+ <View style={[styles.container, styles.rounded, styles.shadow]}>
43
+ ```
44
+
45
+ 2. **Design System**: Tailwind's design tokens ensure consistency
46
+
47
+ 3. **Dark Mode**: Built-in support with `dark:` prefix
48
+
49
+ ```tsx
50
+ <View className="bg-white dark:bg-slate-900">
51
+ ```
52
+
53
+ 4. **Responsive Design**: Support for breakpoints
54
+
55
+ ```tsx
56
+ <View className="p-2 md:p-4 lg:p-6">
57
+ ```
58
+
59
+ 5. **Familiarity**: Web developers already know Tailwind
60
+
61
+ 6. **Performance**: Compiled at build time, no runtime overhead
62
+
63
+ ## Consequences
64
+
65
+ ### Positive
66
+
67
+ - Faster styling with utility classes
68
+ - Consistent spacing, colors, typography
69
+ - Easy dark mode implementation
70
+ - Small runtime footprint
71
+ - Web developers can contribute quickly
72
+
73
+ ### Negative
74
+
75
+ - Long className strings can be hard to read
76
+ - Learning curve for Tailwind utilities
77
+ - Some RN-specific styles need custom config
78
+
79
+ ### Mitigation
80
+
81
+ - Use `cn()` utility to organize classes
82
+ - Create component abstractions for complex patterns
83
+ - Document custom Tailwind config
84
+
85
+ ## Implementation
86
+
87
+ ### Configuration
88
+
89
+ ```js
90
+ // tailwind.config.js
91
+ module.exports = {
92
+ content: ["./app/**/*.{js,tsx}", "./components/**/*.{js,tsx}"],
93
+ presets: [require("nativewind/preset")],
94
+ theme: {
95
+ extend: {
96
+ colors: {
97
+ primary: {
98
+ /* ... */
99
+ },
100
+ background: { light: "#fff", dark: "#0f172a" },
101
+ },
102
+ },
103
+ },
104
+ };
105
+ ```
106
+
107
+ ### Usage Pattern
108
+
109
+ ```tsx
110
+ import { cn } from "@/utils/cn";
111
+
112
+ function Card({ className, ...props }) {
113
+ return (
114
+ <View
115
+ className={cn(
116
+ "p-4 rounded-xl",
117
+ "bg-white dark:bg-slate-800",
118
+ "border border-gray-200 dark:border-gray-700",
119
+ className
120
+ )}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+ ```
126
+
127
+ ## References
128
+
129
+ - [NativeWind Documentation](https://www.nativewind.dev/)
130
+ - [Tailwind CSS](https://tailwindcss.com/)
@@ -0,0 +1,155 @@
1
+ # ADR-003: Use React Query for Data Fetching
2
+
3
+ ## Status
4
+
5
+ Accepted
6
+
7
+ ## Date
8
+
9
+ 2024-01-01
10
+
11
+ ## Context
12
+
13
+ We need a data fetching solution that handles:
14
+
15
+ 1. Caching and cache invalidation
16
+ 2. Loading and error states
17
+ 3. Optimistic updates
18
+ 4. Offline support
19
+ 5. Request deduplication
20
+ 6. Background refetching
21
+
22
+ Options considered:
23
+
24
+ - **fetch/axios only**: Manual state management
25
+ - **SWR**: Lightweight, good for web
26
+ - **React Query (TanStack Query)**: Full-featured
27
+ - **RTK Query**: Redux-based
28
+ - **Apollo Client**: GraphQL-focused
29
+
30
+ ## Decision
31
+
32
+ We chose **React Query (TanStack Query)** for server state management.
33
+
34
+ ## Rationale
35
+
36
+ 1. **Comprehensive Feature Set**
37
+ - Automatic caching and cache invalidation
38
+ - Background refetching
39
+ - Optimistic updates
40
+ - Infinite queries for pagination
41
+ - Offline support with persistence
42
+
43
+ 2. **Excellent Developer Experience**
44
+
45
+ ```tsx
46
+ const { data, isLoading, error } = useQuery({
47
+ queryKey: ["users", userId],
48
+ queryFn: () => api.get(`/users/${userId}`),
49
+ });
50
+ ```
51
+
52
+ 3. **Offline Support**: Works great with AsyncStorage persister
53
+
54
+ ```tsx
55
+ const persister = createAsyncStoragePersister({
56
+ storage: AsyncStorage,
57
+ });
58
+
59
+ <PersistQueryClientProvider
60
+ client={queryClient}
61
+ persistOptions={{ persister }}
62
+ >
63
+ ```
64
+
65
+ 4. **Performance**: Smart refetching, request deduplication
66
+
67
+ 5. **DevTools**: React Query DevTools for debugging
68
+
69
+ ## Consequences
70
+
71
+ ### Positive
72
+
73
+ - No need for manual cache management
74
+ - Automatic loading/error states
75
+ - Works offline with persisted cache
76
+ - Reduces boilerplate significantly
77
+ - Battle-tested and well-documented
78
+
79
+ ### Negative
80
+
81
+ - Learning curve for query keys and cache invalidation
82
+ - Can be overkill for simple apps
83
+ - Adds bundle size (~12KB gzipped)
84
+
85
+ ### Mitigation
86
+
87
+ - Create query key factories for consistency
88
+ - Document common patterns
89
+ - Create custom hooks for reusable queries
90
+
91
+ ## Implementation
92
+
93
+ ### Query Client Configuration
94
+
95
+ ```ts
96
+ // services/queryClient.ts
97
+ export const queryClient = new QueryClient({
98
+ defaultOptions: {
99
+ queries: {
100
+ staleTime: 5 * 60 * 1000, // 5 minutes
101
+ gcTime: 30 * 60 * 1000, // 30 minutes
102
+ retry: 3,
103
+ refetchOnReconnect: true,
104
+ },
105
+ },
106
+ });
107
+ ```
108
+
109
+ ### Query Keys Factory
110
+
111
+ ```ts
112
+ // hooks/useApi.ts
113
+ export const queryKeys = {
114
+ users: {
115
+ all: ["users"] as const,
116
+ detail: (id: string) => ["users", id] as const,
117
+ me: () => ["users", "me"] as const,
118
+ },
119
+ posts: {
120
+ all: ["posts"] as const,
121
+ list: (filters: PostFilters) => ["posts", filters] as const,
122
+ detail: (id: string) => ["posts", id] as const,
123
+ },
124
+ };
125
+ ```
126
+
127
+ ### Custom Hook Pattern
128
+
129
+ ```ts
130
+ export function useUser(userId: string) {
131
+ return useQuery({
132
+ queryKey: queryKeys.users.detail(userId),
133
+ queryFn: () => api.get<User>(`/users/${userId}`),
134
+ enabled: !!userId,
135
+ });
136
+ }
137
+
138
+ export function useUpdateUser() {
139
+ const queryClient = useQueryClient();
140
+
141
+ return useMutation({
142
+ mutationFn: (data: UpdateUserInput) => api.patch<User>("/users/me", data),
143
+ onSuccess: () => {
144
+ queryClient.invalidateQueries({
145
+ queryKey: queryKeys.users.me(),
146
+ });
147
+ },
148
+ });
149
+ }
150
+ ```
151
+
152
+ ## References
153
+
154
+ - [TanStack Query Documentation](https://tanstack.com/query)
155
+ - [React Query Offline Support](https://tanstack.com/query/latest/docs/react/plugins/persistQueryClient)