242 lines
6.5 KiB
Markdown
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();
|
|
});
|
|
});
|
|
``` |