@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.
- package/.env.example +18 -0
- package/.eslintrc.js +55 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/eas-build.yml +55 -0
- package/.github/workflows/eas-update.yml +50 -0
- package/.gitignore +62 -0
- package/.prettierrc +11 -0
- package/.storybook/main.ts +28 -0
- package/.storybook/preview.tsx +30 -0
- package/CHANGELOG.md +106 -0
- package/CONTRIBUTING.md +377 -0
- package/README.md +399 -0
- package/__tests__/components/Button.test.tsx +74 -0
- package/__tests__/hooks/useAuth.test.tsx +499 -0
- package/__tests__/services/api.test.ts +535 -0
- package/__tests__/utils/cn.test.ts +39 -0
- package/app/(auth)/_layout.tsx +36 -0
- package/app/(auth)/home.tsx +117 -0
- package/app/(auth)/profile.tsx +152 -0
- package/app/(auth)/settings.tsx +147 -0
- package/app/(public)/_layout.tsx +21 -0
- package/app/(public)/forgot-password.tsx +127 -0
- package/app/(public)/login.tsx +120 -0
- package/app/(public)/onboarding.tsx +5 -0
- package/app/(public)/register.tsx +139 -0
- package/app/_layout.tsx +97 -0
- package/app/index.tsx +21 -0
- package/app.config.ts +72 -0
- package/assets/images/.gitkeep +7 -0
- package/assets/images/adaptive-icon.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/notification-icon.png +0 -0
- package/assets/images/splash.png +0 -0
- package/babel.config.js +10 -0
- package/components/ErrorBoundary.tsx +169 -0
- package/components/forms/FormInput.tsx +78 -0
- package/components/forms/index.ts +1 -0
- package/components/onboarding/OnboardingScreen.tsx +370 -0
- package/components/onboarding/index.ts +2 -0
- package/components/ui/AnimatedButton.tsx +156 -0
- package/components/ui/AnimatedCard.tsx +108 -0
- package/components/ui/Avatar.tsx +316 -0
- package/components/ui/Badge.tsx +416 -0
- package/components/ui/BottomSheet.tsx +307 -0
- package/components/ui/Button.stories.tsx +115 -0
- package/components/ui/Button.tsx +104 -0
- package/components/ui/Card.stories.tsx +84 -0
- package/components/ui/Card.tsx +32 -0
- package/components/ui/Checkbox.tsx +261 -0
- package/components/ui/Input.stories.tsx +106 -0
- package/components/ui/Input.tsx +117 -0
- package/components/ui/Modal.tsx +98 -0
- package/components/ui/OptimizedImage.tsx +369 -0
- package/components/ui/Select.tsx +240 -0
- package/components/ui/Skeleton.tsx +180 -0
- package/components/ui/index.ts +18 -0
- package/constants/config.ts +54 -0
- package/docs/adr/001-state-management.md +79 -0
- package/docs/adr/002-styling-approach.md +130 -0
- package/docs/adr/003-data-fetching.md +155 -0
- package/docs/adr/004-auth-adapter-pattern.md +144 -0
- package/docs/adr/README.md +78 -0
- package/eas.json +47 -0
- package/global.css +10 -0
- package/hooks/index.ts +25 -0
- package/hooks/useApi.ts +236 -0
- package/hooks/useAuth.tsx +290 -0
- package/hooks/useBiometrics.ts +295 -0
- package/hooks/useDeepLinking.ts +256 -0
- package/hooks/useNotifications.ts +138 -0
- package/hooks/useOffline.ts +69 -0
- package/hooks/usePerformance.ts +434 -0
- package/hooks/useTheme.tsx +85 -0
- package/hooks/useUpdates.ts +358 -0
- package/i18n/index.ts +77 -0
- package/i18n/locales/en.json +101 -0
- package/i18n/locales/fr.json +101 -0
- package/jest.config.js +32 -0
- package/maestro/README.md +113 -0
- package/maestro/config.yaml +35 -0
- package/maestro/flows/login.yaml +62 -0
- package/maestro/flows/navigation.yaml +68 -0
- package/maestro/flows/offline.yaml +60 -0
- package/maestro/flows/register.yaml +94 -0
- package/metro.config.js +6 -0
- package/nativewind-env.d.ts +1 -0
- package/package.json +170 -0
- package/scripts/init.ps1 +162 -0
- package/scripts/init.sh +174 -0
- package/services/analytics.ts +428 -0
- package/services/api.ts +340 -0
- package/services/authAdapter.ts +333 -0
- package/services/index.ts +22 -0
- package/services/queryClient.ts +97 -0
- package/services/sentry.ts +131 -0
- package/services/storage.ts +82 -0
- package/stores/appStore.ts +54 -0
- package/stores/index.ts +2 -0
- package/stores/notificationStore.ts +40 -0
- package/tailwind.config.js +47 -0
- package/tsconfig.json +26 -0
- package/types/index.ts +42 -0
- package/types/user.ts +63 -0
- package/utils/accessibility.ts +446 -0
- package/utils/cn.ts +14 -0
- package/utils/index.ts +43 -0
- package/utils/toast.ts +113 -0
- 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)
|