playbook/antigravity-awesome-skills/skills/android-dev/references/react-native.md

242 lines
6.5 KiB
Markdown

# React Native Reference (TypeScript)
## Project Structure
```
src/
├── app/
│ ├── App.tsx # Root component, providers
│ ├── navigation/ # React Navigation stacks + types
│ └── store/ # RTK store setup
├── features/
│ └── home/
│ ├── api/ # RTK Query endpoints
│ ├── components/ # Screen-specific components
│ ├── hooks/ # Feature-level custom hooks
│ ├── screens/ # Screen components
│ ├── store/ # Zustand slice or RTK slice
│ └── types.ts # Feature types
├── shared/
│ ├── components/ # Design system components
│ ├── hooks/ # Shared hooks
│ ├── theme/ # Colors, typography, spacing constants
│ └── utils/ # Utilities
└── services/
├── api/ # Axios/fetch client + interceptors
└── storage/ # MMKV wrapper
```
## Navigation Setup (React Navigation v7)
```typescript
export type RootStackParamList = {
Auth: undefined;
Home: undefined;
Detail: { id: string };
Settings: undefined;
};
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
const Stack = createNativeStackNavigator<RootStackParamList>();
export const RootNavigator = () => {
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isLoggedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Detail" component={DetailScreen} />
</>
) : (
<Stack.Screen name="Auth" component={AuthScreen} />
)}
</Stack.Navigator>
);
};
```
## State Management (Zustand + React Query)
```typescript
// Client state — Zustand
interface AuthState {
token: string | null;
isLoggedIn: boolean;
setToken: (token: string) => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
isLoggedIn: false,
setToken: (token) => set({ token, isLoggedIn: true }),
logout: () => set({ token: null, isLoggedIn: false }),
}),
{ name: 'auth-storage', storage: createJSONStorage(() => mmkvStorage) }
)
);
// Server state — React Query
export const useItems = () =>
useQuery({
queryKey: ['items'],
queryFn: itemsApi.getAll,
staleTime: 5 * 60 * 1000, // 5 minutes
});
export const useRefreshItems = () =>
useMutation({
mutationFn: itemsApi.refresh,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
});
```
## Screen Pattern
```typescript
type HomeScreenProps = RootStackScreenProps<'Home'>;
export const HomeScreen: FC<HomeScreenProps> = ({ navigation }) => {
const { data: items, isLoading, isError, refetch } = useItems();
if (isLoading) return <LoadingView />;
if (isError) return <ErrorView onRetry={refetch} />;
return (
<SafeAreaView style={styles.container}>
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ItemCard
item={item}
onPress={() => navigation.navigate('Detail', { id: item.id })}
/>
)}
ListEmptyComponent={<EmptyView />}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={refetch} />
}
/>
</SafeAreaView>
);
};
```
## API Client (Axios with interceptors)
```typescript
const apiClient = axios.create({
baseURL: Config.API_BASE_URL,
timeout: 10_000,
headers: { 'Content-Type': 'application/json' },
});
// Auth token injection
apiClient.interceptors.request.use((config) => {
const token = useAuthStore.getState().token;
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Token refresh on 401
apiClient.interceptors.response.use(
(res) => res,
async (error: AxiosError) => {
if (error.response?.status === 401) {
const newToken = await refreshToken();
if (newToken) {
useAuthStore.getState().setToken(newToken);
return apiClient(error.config!);
}
useAuthStore.getState().logout();
}
return Promise.reject(error);
}
);
```
## API Response Validation (Zod)
```typescript
const ItemSchema = z.object({
id: z.string(),
title: z.string(),
description: z.string().optional(),
createdAt: z.string().datetime(),
});
const ItemsResponseSchema = z.array(ItemSchema);
type Item = z.infer<typeof ItemSchema>;
const getItems = async (): Promise<Item[]> => {
const { data } = await apiClient.get('/items');
return ItemsResponseSchema.parse(data); // throws ZodError on invalid shape
};
```
## Key Dependencies
```json
{
"dependencies": {
"react-native": "0.74.x",
"@react-navigation/native": "^7.0.0",
"@react-navigation/native-stack": "^7.0.0",
"@tanstack/react-query": "^5.45.0",
"zustand": "^4.5.4",
"axios": "^1.7.2",
"zod": "^3.23.8",
"react-native-mmkv": "^2.12.2",
"react-native-safe-area-context": "^4.10.1",
"react-native-screens": "^3.32.0"
},
"devDependencies": {
"typescript": "^5.4.5",
"@testing-library/react-native": "^12.5.1",
"msw": "^2.3.1",
"jest": "^29.7.0"
}
}
```
## New Architecture (Bridgeless) Notes
- Enable New Architecture in `android/gradle.properties`: `newArchEnabled=true`
- Use TurboModules for native modules; avoid legacy NativeModules API
- Use Fabric for custom native views
- Test with Hermes JS engine always enabled
## Performance Tips
- Use `useCallback` + `memo` on `renderItem` / list item components
- `FlatList` `windowSize`, `initialNumToRender`, `maxToRenderPerBatch` tuned
- Avoid anonymous inline functions in JSX
- `InteractionManager.runAfterInteractions` for heavy post-navigation work
- `react-native-reanimated` for 60fps animations (runs on UI thread)
## Testing
```typescript
describe('HomeScreen', () => {
it('shows items when query succeeds', async () => {
server.use(
http.get(`${API_URL}/items`, () =>
HttpResponse.json([{ id: '1', title: 'Test Item' }])
)
);
const { getByText } = render(
<QueryClientProvider client={testQueryClient}>
<HomeScreen navigation={mockNavigation} route={mockRoute} />
</QueryClientProvider>
);
expect(await findByText('Test Item')).toBeTruthy();
});
});
```