Loading...
Loading...
Compare original and translation side by side
react-advancedreact-advanced| Web Library | RN Equivalent | Key Difference |
|---|---|---|
| TanStack Router | Expo Router | No route loaders on native, file-based navigation |
| TanStack Start | — | No SSR/server functions on native |
| TanStack Virtual | FlashList | Native view recycling, not DOM virtualization |
| localStorage | MMKV | Synchronous, native-thread, 30x faster |
| window events | AppState/NetInfo | Manual wiring required for focus/online managers |
| CSS animations | Reanimated | UI-thread worklets, CSS transitions (v4) |
| DOM events | Gesture Handler | Gesture composition API, UI-thread callbacks |
| expo-image | SDWebImage/Glide, blurhash, disk caching |
| Web Push API | expo-notifications | FCM/APNs, channels, background tasks |
| Service Workers | expo-updates | OTA updates, staged rollout, emergency rollback |
| Web类库 | RN等价实现 | 核心差异 |
|---|---|---|
| TanStack Router | Expo Router | 原生端无路由加载器,采用基于文件的导航 |
| TanStack Start | — | 原生端无SSR/服务端函数 |
| TanStack Virtual | FlashList | 原生视图回收,而非DOM虚拟化 |
| localStorage | MMKV | 同步执行、原生线程运行,速度快30倍 |
| window events | AppState/NetInfo | 焦点/在线状态管理器需要手动绑定 |
| CSS animations | Reanimated | UI线程工作流,支持CSS过渡(v4版本) |
| DOM events | Gesture Handler | 手势组合API,UI线程回调 |
| expo-image | 基于SDWebImage/Glide实现,支持模糊哈希、磁盘缓存 |
| Web Push API | expo-notifications | 支持FCM/APNs、通知渠道、后台任务 |
| Service Workers | expo-updates | OTA更新、分阶段发布、紧急回滚 |
// hooks/useAppState.ts
import { useEffect } from "react";
import { AppState, Platform } from "react-native";
import type { AppStateStatus } from "react-native";
import { focusManager } from "@tanstack/react-query";
function onAppStateChange(status: AppStateStatus) {
if (Platform.OS !== "web") {
focusManager.setFocused(status === "active");
}
}
export function useAppState() {
useEffect(() => {
const sub = AppState.addEventListener("change", onAppStateChange);
return () => sub.remove();
}, []);
}// hooks/useAppState.ts
import { useEffect } from "react";
import { AppState, Platform } from "react-native";
import type { AppStateStatus } from "react-native";
import { focusManager } from "@tanstack/react-query";
function onAppStateChange(status: AppStateStatus) {
if (Platform.OS !== "web") {
focusManager.setFocused(status === "active");
}
}
export function useAppState() {
useEffect(() => {
const sub = AppState.addEventListener("change", onAppStateChange);
return () => sub.remove();
}, []);
}// hooks/useOnlineManager.ts
import { useEffect } from "react";
import NetInfo from "@react-native-community/netinfo";
import { onlineManager } from "@tanstack/react-query";
export function useOnlineManager() {
useEffect(() => {
return NetInfo.addEventListener((state) => {
onlineManager.setOnline(!!state.isConnected);
});
}, []);
}// app/_layout.tsx
export default function RootLayout() {
useAppState()
useOnlineManager()
return (
<QueryClientProvider client={queryClient}>
<Slot />
</QueryClientProvider>
)
}// hooks/useOnlineManager.ts
import { useEffect } from "react";
import NetInfo from "@react-native-community/netinfo";
import { onlineManager } from "@tanstack/react-query";
export function useOnlineManager() {
useEffect(() => {
return NetInfo.addEventListener((state) => {
onlineManager.setOnline(!!state.isConnected);
});
}, []);
}// app/_layout.tsx
export default function RootLayout() {
useAppState()
useOnlineManager()
return (
<QueryClientProvider client={queryClient}>
<Slot />
</QueryClientProvider>
)
}function PostListItem({ id }: { id: string }) {
const queryClient = useQueryClient()
const router = useRouter()
return (
<Pressable
onPress={() => {
queryClient.prefetchQuery(postQueryOptions(id)) // fire-and-forget
router.push(`/posts/${id}`)
}}
>
<Text>{title}</Text>
</Pressable>
)
}function PostListItem({ id }: { id: string }) {
const queryClient = useQueryClient()
const router = useRouter()
return (
<Pressable
onPress={() => {
queryClient.prefetchQuery(postQueryOptions(id)) // 触发后无需等待
router.push(`/posts/${id}`)
}}
>
<Text>{title}</Text>
</Pressable>
)
}useEffectuseFocusEffectimport { useFocusEffect } from "expo-router";
export function useRefreshOnFocus(queryKey: unknown[]) {
const queryClient = useQueryClient();
const firstRender = useRef(true);
useFocusEffect(
useCallback(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
queryClient.invalidateQueries({ queryKey });
}, [queryClient, queryKey]),
);
}invalidateQueriesstaleTimerefetch()useEffectuseFocusEffectimport { useFocusEffect } from "expo-router";
export function useRefreshOnFocus(queryKey: unknown[]) {
const queryClient = useQueryClient();
const firstRender = useRef(true);
useFocusEffect(
useCallback(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
queryClient.invalidateQueries({ queryKey });
}, [queryClient, queryKey]),
);
}invalidateQueriesstaleTimerefetch()// app/_layout.tsx
import { Stack } from 'expo-router'
import { useAuthStore } from '@/stores/authStore'
export default function RootLayout() {
const session = useAuthStore((s) => s.session)
return (
<Stack>
<Stack.Protected guard={!!session}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack.Protected>
<Stack.Protected guard={!session}>
<Stack.Screen name="sign-in" />
</Stack.Protected>
</Stack>
)
}session// app/_layout.tsx
import { Stack } from 'expo-router'
import { useAuthStore } from '@/stores/authStore'
export default function RootLayout() {
const session = useAuthStore((s) => s.session)
return (
<Stack>
<Stack.Protected guard={!!session}>
<Stack.Screen name="(tabs)" />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack.Protected>
<Stack.Protected guard={!session}>
<Stack.Screen name="sign-in" />
</Stack.Protected>
</Stack>
)
}sessionStack.Protectedconst AuthContext = createActorContext(authMachine)
function RootNavigator() {
const session = AuthContext.useSelector((s) => s.context.session)
const isChecking = AuthContext.useSelector((s) => s.matches('checking'))
if (isChecking) return <SplashScreen />
return (
<Stack>
<Stack.Protected guard={!!session}>
<Stack.Screen name="(app)" />
</Stack.Protected>
<Stack.Protected guard={!session}>
<Stack.Screen name="sign-in" />
</Stack.Protected>
</Stack>
)
}Stack.Protectedconst AuthContext = createActorContext(authMachine)
function RootNavigator() {
const session = AuthContext.useSelector((s) => s.context.session)
const isChecking = AuthContext.useSelector((s) => s.matches('checking'))
if (isChecking) return <SplashScreen />
return (
<Stack>
<Stack.Protected guard={!!session}>
<Stack.Screen name="(app)" />
</Stack.Protected>
<Stack.Protected guard={!session}>
<Stack.Screen name="sign-in" />
</Stack.Protected>
</Stack>
)
}function PostList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isRefetching } =
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
})
const items = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data])
const handleEndReached = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) fetchNextPage()
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
return (
<FlashList
data={items}
renderItem={({ item }) => <PostCard post={item} />}
keyExtractor={(item) => item.id}
onEndReached={handleEndReached}
onEndReachedThreshold={0.3}
ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} />}
/>
)
}!isFetchingNextPagehandleEndReachedonEndReachedfunction PostList() {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isRefetching } =
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
})
const items = useMemo(() => data?.pages.flatMap((p) => p.items) ?? [], [data])
const handleEndReached = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) fetchNextPage()
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
return (
<FlashList
data={items}
renderItem={({ item }) => <PostCard post={item} />}
keyExtractor={(item) => item.id}
onEndReached={handleEndReached}
onEndReachedThreshold={0.3}
ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
refreshControl={<RefreshControl refreshing={isRefetching} onRefresh={refetch} />}
/>
)
}handleEndReached!isFetchingNextPageonEndReachedTextInputonChangeTextonChange<form.Field name="username">
{(field) => (
<TextInput
value={field.state.value}
onChangeText={field.handleChange} // string directly — no event extraction
onBlur={field.handleBlur} // triggers isTouched + onBlur validators
autoCapitalize="none"
/>
)}
</form.Field><TextInput
keyboardType="numeric"
onChangeText={(val) => field.handleChange(val === '' ? null : Number(val))}
value={String(field.state.value ?? '')}
/>ScrollViewkeyboardShouldPersistTaps="handled"TextInputonChangeTextonChange<form.Field name="username">
{(field) => (
<TextInput
value={field.state.value}
onChangeText={field.handleChange} // 直接传入字符串 —— 无需提取事件值
onBlur={field.handleBlur} // 触发isTouched状态和onBlur验证器
autoCapitalize="none"
/>
)}
</form.Field><TextInput
keyboardType="numeric"
onChangeText={(val) => field.handleChange(val === '' ? null : Number(val))}
value={String(field.state.value ?? '')}
/>keyboardShouldPersistTaps="handled"ScrollViewimport { createMMKV } from "react-native-mmkv";
import { StateStorage, createJSONStorage } from "zustand/middleware";
const mmkv = createMMKV(); // create at module level — never inside a component
const zustandStorage: StateStorage = {
setItem: (name, value) => mmkv.set(name, value),
getItem: (name) => mmkv.getString(name) ?? null, // must return null, not undefined
removeItem: (name) => mmkv.remove(name),
};
export const useAppStore = create<AppState>()(
persist(
(set) => ({
/* state + actions */
}),
{
name: "app-storage",
storage: createJSONStorage(() => zustandStorage),
partialize: (state) => ({ token: state.token, theme: state.theme }),
},
),
);encryptionKeyreferences/expo-essentials.mdimport { createMMKV } from "react-native-mmkv";
import { StateStorage, createJSONStorage } from "zustand/middleware";
const mmkv = createMMKV(); // 在模块级别创建 —— 永远不要在组件内部创建
const zustandStorage: StateStorage = {
setItem: (name, value) => mmkv.set(name, value),
getItem: (name) => mmkv.getString(name) ?? null, // 必须返回null,不能返回undefined
removeItem: (name) => mmkv.remove(name),
};
export const useAppStore = create<AppState>()(
persist(
(set) => ({
/* 状态 + 操作 */
}),
{
name: "app-storage",
storage: createJSONStorage(() => zustandStorage),
partialize: (state) => ({ token: state.token, theme: state.theme }),
},
),
);encryptionKeyreferences/expo-essentials.md.valueconst x = useSharedValue(0);
// Gesture callback — runs on UI thread (worklet)
const pan = Gesture.Pan()
.onChange((e) => {
"worklet";
x.value += e.changeX; // instant, no bridge
})
.onEnd(() => {
"worklet";
x.value = withSpring(0);
runOnJS(onDragEnd)(); // call JS functions via runOnJS
});
// Animated style — also runs on UI thread
const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }],
}));.valueconst x = useSharedValue(0);
// 手势回调 —— 在UI线程运行(工作流)
const pan = Gesture.Pan()
.onChange((e) => {
"worklet";
x.value += e.changeX; // 即时执行,无桥接开销
})
.onEnd(() => {
"worklet";
x.value = withSpring(0);
runOnJS(onDragEnd)(); // 通过runOnJS调用JS函数
});
// 动画样式 —— 同样在UI线程运行
const style = useAnimatedStyle(() => ({
transform: [{ translateX: x.value }],
}));sv.value = { x: 50 }sv.value.x = 50const { x } = sv.valuestyle={{ flex: 1 }}GestureHandlerRootViewsv.value = { x: 50 }sv.value.x = 50const { x } = sv.valuestyle={{ flex: 1 }}GestureHandlerRootViewenteringexitingreferences/animations.mdenteringexitingreferences/animations.md@lodev09/react-native-true-sheetUISheetPresentationControllerBottomSheetDialog@lodev09/react-native-true-sheetUISheetPresentationControllerBottomSheetDialogimport { TrueSheet } from "@lodev09/react-native-true-sheet";
function MySheet() {
const sheet = useRef<TrueSheet>(null);
return (
<>
<Button onPress={() => sheet.current?.present()} />
<TrueSheet ref={sheet} detents={[0.5, 1]}>
<MyContent />
</TrueSheet>
</>
);
}import { TrueSheet } from "@lodev09/react-native-true-sheet";
function MySheet() {
const sheet = useRef<TrueSheet>(null);
return (
<>
<Button onPress={() => sheet.current?.present()} />
<TrueSheet ref={sheet} detents={[0.5, 1]}>
<MyContent />
</TrueSheet>
</>
);
}ref.present()dismiss()resize()'auto'scrollable'auto'flex: 1flexGrowdismiss()TextInputScrollViewFlashListscrollablenestedScrollEnabledreferences/bottom-sheet.mdref.present()dismiss()resize()'auto'scrollable'auto'flex: 1flexGrowdismiss()TextInputScrollViewFlashListscrollablenestedScrollEnabledreferences/bottom-sheet.mdapp.jsonpluginsapp.jsonplugins// Must configure or foreground notifications are silently suppressed
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});// 必须配置,否则前台通知会被静默抑制
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});references/notifications.mdreferences/notifications.mdimport * as Updates from "expo-updates";
// Run after first render, not during startup (can freeze UI on slow network)
const check = await Updates.checkForUpdateAsync();
if (check.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}import * as Updates from "expo-updates";
// 在首次渲染后运行,不要在启动时运行(网络慢时会冻结UI)
const check = await Updates.checkForUpdateAsync();
if (check.isAvailable) {
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}if (Updates.isEmergencyLaunch) {
// OTA caused a crash — app rolled back to embedded bundle
// Log immediately to error tracking
}references/expo-essentials.mdif (Updates.isEmergencyLaunch) {
// OTA导致崩溃 —— 应用回滚到内置包
// 立即上报到错误跟踪系统
}references/expo-essentials.mdapp/
_layout.tsx # Root layout — providers, auth guard
(auth)/
_layout.tsx # Auth group layout
sign-in.tsx
(app)/
_layout.tsx # App group layout (tabs)
(tabs)/
_layout.tsx # Tab navigator
index.tsx
profile.tsx
[id].tsx # Dynamic route
modal.tsx # Modal screen
queries/ # queryOptions definitions
mutations/ # useMutation wrappers
machines/ # XState machine definitions
stores/ # Zustand stores (MMKV persist)
hooks/ # useAppState, useOnlineManager, useRefreshOnFocus
components/ # Shared components(name)/_layout.tsxqueries/queryOptionsapp/
_layout.tsx # 根布局 —— 提供者、身份验证守卫
(auth)/
_layout.tsx # 身份验证组布局
sign-in.tsx
(app)/
_layout.tsx # 应用组布局(标签栏)
(tabs)/
_layout.tsx # 标签导航器
index.tsx
profile.tsx
[id].tsx # 动态路由
modal.tsx # 模态页面
queries/ # queryOptions定义
mutations/ # useMutation封装
machines/ # XState状态机定义
stores/ # Zustand状态(MMKV持久化
hooks/ # useAppState, useOnlineManager, useRefreshOnFocus
components/ # 共享组件(name)/_layout.tsxqueries/queryOptionsrefetchOnWindowFocusprefetchQueryrouter.pushuseGlobalSearchParamsuseLocalSearchParamsuseEffectuseEffectuseFocusEffectexpo-routergetNextPageParamnullundefinedlastPage.nextCursor ?? undefinedestimatedItemSizeoverrideItemLayoutspangetStringundefinedStateStorage.getItemnull?? nullkeyboardShouldPersistTapsretry: 3retry: 1sv.value.x = 50sv.value = { x: 50, y: 0 }flex: 1TrueSheetdismiss()autoscrollablescrollable={true}setNotificationHandlercheckForUpdateAsyncrecyclingKeyrequireAuthenticationrefetchOnWindowFocusrouter.pushprefetchQueryuseGlobalSearchParamsuseLocalSearchParamsuseEffectuseEffectexpo-routeruseFocusEffectgetNextPageParamnullundefinedlastPage.nextCursor ?? undefinedestimatedItemSizeoverrideItemLayoutspangetStringundefinedStateStorage.getItemnull?? nullkeyboardShouldPersistTapsretry: 3retry: 1sv.value.x = 50sv.value = { x: 50, y: 0 }flex: 1TrueSheetdismiss()autoscrollablesetNotificationHandlercheckForUpdateAsyncrecyclingKeyrequireAuthentication| File | When to read |
|---|---|
| Focus/online managers, cache persistence, prefetching |
| Typed routes, layouts, modals, auth, search params |
| FlashList + React Query, infinite scroll, performance |
| MMKV persist adapter, encryption, hydration patterns |
| RNTL, testing Query/Router/Form/XState, MSW in RN |
| Reanimated, Gesture Handler patterns and gotchas |
| react-native-true-sheet setup, detents, Expo Router |
| expo-notifications permissions, listeners, background |
| expo-image, expo-secure-store, expo-haptics, expo-updates |
| 文件 | 适用场景 |
|---|---|
| 焦点/在线管理器、缓存持久化、预取 |
| 类型路由、布局、模态框、身份验证、搜索参数 |
| FlashList + React Query、无限滚动、性能优化 |
| MMKV持久化适配器、加密、hydration模式 |
| RNTL、测试Query/Router/Form/XState、RN中的MSW |
| Reanimated、Gesture Handler模式和注意事项 |
| react-native-true-sheet配置、停靠点、Expo Router |
| expo-notifications权限、监听器、后台任务 |
| expo-image、expo-secure-store、expo-haptics、expo-updates |