expo-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

When to use

适用场景

Use this skill when working with Expo or React Native code. It teaches current best practices and prevents common mistakes that AI agents make with outdated patterns, manual native setup, and deprecated APIs.
当你处理Expo或React Native代码时可以使用这份指南,它讲解了当前的最佳实践,避免AI Agent使用过时模式、手动原生配置以及废弃API所导致的常见错误。

Critical Rules

核心规则

1. Use Expo Router for navigation

1. 使用Expo Router实现导航

Wrong (agents do this):
tsx
import { createStackNavigator } from "@react-navigation/stack";
const Stack = createStackNavigator();
export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
Correct:
app/
  _layout.tsx
  index.tsx
  settings.tsx
Use the
app/
directory convention. Files become routes automatically. New Expo projects include Expo Router by default.
Why: Expo Router is built on React Navigation, provides file-based routing, typed routes, deep linking, and integrates with Expo CLI. Manual React Navigation setup is redundant and misses Expo Router features.
错误写法(Agent常犯的问题):
tsx
import { createStackNavigator } from "@react-navigation/stack";
const Stack = createStackNavigator();
export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
正确写法:
app/
  _layout.tsx
  index.tsx
  settings.tsx
遵循
app/
目录约定,文件会自动成为路由。新的Expo项目默认已经集成了Expo Router。
原因: Expo Router基于React Navigation构建,提供了文件路由、类型化路由、深度链接能力,并且与Expo CLI深度集成。手动配置React Navigation是冗余操作,还会错失Expo Router的各项特性。

2. Use expo-router layouts for navigation structure

2. 使用expo-router布局文件定义导航结构

Wrong:
tsx
// Each screen manually defines its own layout
Correct:
tsx
// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
  return <Stack />;
}
Why:
_layout.tsx
defines shared navigation structure, tabs, stacks, and nested layouts. It centralizes navigation configuration.
错误写法:
tsx
// Each screen manually defines its own layout
正确写法:
tsx
// app/_layout.tsx
import { Stack } from "expo-router";
export default function RootLayout() {
  return <Stack />;
}
原因:
_layout.tsx
用于定义共享的导航结构、标签栏、堆栈以及嵌套布局,可以集中管理导航配置。

3. Use EAS Build for native builds

3. 使用EAS Build做原生构建

Wrong:
bash
expo build:ios
expo build:android
Correct:
bash
eas build --platform ios
eas build --platform android
Why:
expo build
is deprecated. EAS Build is the current cloud build service with better caching, credentials management, and native module support.
错误写法:
bash
expo build:ios
expo build:android
正确写法:
bash
eas build --platform ios
eas build --platform android
原因:
expo build
已经废弃,EAS Build是当前官方的云构建服务,拥有更好的缓存、凭证管理以及原生模块支持能力。

4. Use Expo config plugins for native configuration

4. 使用Expo配置插件处理原生配置

Wrong:
bash
undefined
错误写法:
bash
undefined

Manually editing

Manually editing

ios/MyApp/Info.plist android/app/src/main/AndroidManifest.xml

**Correct:**

```ts
// app.config.ts
import { ConfigPlugin } from "expo/config-plugins";
const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => {
  config = withInfoPlist(config, (c) => {
    c.modResults["MY_API_KEY"] = apiKey;
    return c;
  });
  return config;
};
export default {
  plugins: [["withMyApiKey", { apiKey: process.env.API_KEY }]],
};
Why: Direct edits to native files are overwritten on prebuild. Config plugins modify native files at build time and survive prebuild.
ios/MyApp/Info.plist android/app/src/main/AndroidManifest.xml

**正确写法:**

```ts
// app.config.ts
import { ConfigPlugin } from "expo/config-plugins";
const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => {
  config = withInfoPlist(config, (c) => {
    c.modResults["MY_API_KEY"] = apiKey;
    return c;
  });
  return config;
};
export default {
  plugins: [["withMyApiKey", { apiKey: process.env.API_KEY }]],
};
原因: 直接修改原生文件会在预构建时被覆盖,配置插件会在构建时修改原生文件,在预构建后仍然生效。

5. Platform-specific code: use platform extensions or Platform.select

5. 平台特定代码:使用平台扩展名或Platform.select

Wrong:
tsx
if (Platform.OS === "ios") {
  return <IOSComponent />;
}
return <AndroidComponent />;
Correct:
MyComponent.ios.tsx
MyComponent.android.tsx
Or:
tsx
import { Platform } from "react-native";
const styles = StyleSheet.create({
  container: Platform.select({
    ios: { paddingTop: 20 },
    android: { paddingTop: 0 },
  }),
});
Why: Platform file extensions let the bundler include only the correct code per platform. Platform.select keeps conditional logic in one file when appropriate.
错误写法:
tsx
if (Platform.OS === "ios") {
  return <IOSComponent />;
}
return <AndroidComponent />;
正确写法:
MyComponent.ios.tsx
MyComponent.android.tsx
或者:
tsx
import { Platform } from "react-native";
const styles = StyleSheet.create({
  container: Platform.select({
    ios: { paddingTop: 20 },
    android: { paddingTop: 0 },
  }),
});
原因: 平台文件扩展名可以让打包工具只引入对应平台的正确代码,适合的场景下使用Platform.select可以把条件逻辑统一放在单个文件中。

6. Use expo-image instead of React Native Image

6. 使用expo-image替代React Native原生Image

Wrong:
tsx
import { Image } from "react-native";
<Image source={{ uri: url }} style={styles.image} />;
Correct:
tsx
import { Image } from "expo-image";
<Image source={url} contentFit="cover" style={styles.image} />;
Why: expo-image adds caching, placeholders, blurhash, and better performance. React Native's Image lacks these features.
错误写法:
tsx
import { Image } from "react-native";
<Image source={{ uri: url }} style={styles.image} />;
正确写法:
tsx
import { Image } from "expo-image";
<Image source={url} contentFit="cover" style={styles.image} />;
原因: expo-image额外提供了缓存、占位图、blurhash以及更好的性能表现,React Native原生Image不具备这些特性。

7. Use expo-font for custom fonts

7. 使用expo-font加载自定义字体

Wrong:
bash
undefined
错误写法:
bash
undefined

Manual font linking via native projects

Manual font linking via native projects


**Correct:**

```tsx
import * as Font from "expo-font";
await Font.loadAsync({
  CustomFont: require("./assets/fonts/CustomFont.ttf"),
});
Why: expo-font handles loading and avoids manual native configuration.

**正确写法:**

```tsx
import * as Font from "expo-font";
await Font.loadAsync({
  CustomFont: require("./assets/fonts/CustomFont.ttf"),
});
原因: expo-font会自动处理字体加载,无需手动修改原生配置。

8. Use expo-secure-store for sensitive data

8. 使用expo-secure-store存储敏感数据

Wrong:
tsx
import AsyncStorage from "@react-native-async-storage/async-storage";
await AsyncStorage.setItem("authToken", token);
Correct:
tsx
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("authToken", token);
Why: AsyncStorage is not encrypted. expo-secure-store uses the platform keychain/Keystore for tokens and secrets.
错误写法:
tsx
import AsyncStorage from "@react-native-async-storage/async-storage";
await AsyncStorage.setItem("authToken", token);
正确写法:
tsx
import * as SecureStore from "expo-secure-store";
await SecureStore.setItemAsync("authToken", token);
原因: AsyncStorage没有加密,expo-secure-store使用平台的keychain/Keystore来存储token和密钥。

9. Use expo-notifications for push notifications

9. 使用expo-notifications实现推送通知

Wrong:
tsx
// Manual Firebase/APNs setup, native configuration
Correct:
tsx
import * as Notifications from "expo-notifications";
const token = await Notifications.getExpoPushTokenAsync();
Why: expo-notifications abstracts push setup across iOS and Android. Manual Firebase/APNs configuration is error-prone and platform-specific.
错误写法:
tsx
// Manual Firebase/APNs setup, native configuration
正确写法:
tsx
import * as Notifications from "expo-notifications";
const token = await Notifications.getExpoPushTokenAsync();
原因: expo-notifications封装了iOS和Android两端的推送配置逻辑,手动配置Firebase/APNs容易出错,且强依赖平台特性。

10. Use app.json or app.config.ts for configuration

10. 使用app.json或app.config.ts做配置管理

Wrong:
bash
undefined
错误写法:
bash
undefined

Editing Info.plist or AndroidManifest.xml directly

Editing Info.plist or AndroidManifest.xml directly


**Correct:**

```json
{
  "expo": {
    "name": "MyApp",
    "slug": "my-app",
    "plugins": ["expo-router"]
  }
}
Why: Expo manages native config from app.json/app.config.ts. Direct edits are overwritten on prebuild.

**正确写法:**

```json
{
  "expo": {
    "name": "MyApp",
    "slug": "my-app",
    "plugins": ["expo-router"]
  }
}
原因: Expo会从app.json/app.config.ts中读取配置来管理原生配置,直接修改原生配置文件会在预构建时被覆盖。

11. Use expo-updates for OTA updates

11. 使用expo-updates实现OTA更新

Wrong:
tsx
import codePush from "react-native-code-push";
export default codePush(MyApp);
Correct:
tsx
import * as Updates from "expo-updates";
await Updates.checkForUpdateAsync();
Why: expo-updates integrates with EAS and Expo's update service. CodePush requires separate setup and is redundant in Expo projects.
错误写法:
tsx
import codePush from "react-native-code-push";
export default codePush(MyApp);
正确写法:
tsx
import * as Updates from "expo-updates";
await Updates.checkForUpdateAsync();
原因: expo-updates与EAS和Expo的更新服务深度集成,CodePush需要单独配置,在Expo项目中属于冗余方案。

12. Use TypeScript with typed navigation params

12. 结合TypeScript使用类型化导航参数

Wrong:
tsx
router.push("/profile");
Correct:
tsx
import { router } from "expo-router";
router.push({ pathname: "/profile/[id]", params: { id: userId } });
Define route params in your route files. Expo Router provides typed route helpers when used with TypeScript.
Why: Typed params prevent runtime errors and improve editor support.
错误写法:
tsx
router.push("/profile");
正确写法:
tsx
import { router } from "expo-router";
router.push({ pathname: "/profile/[id]", params: { id: userId } });
在路由文件中定义路由参数,Expo Router配合TypeScript使用时会提供类型化路由辅助方法。
原因: 类型化参数可以避免运行时错误,提升编辑器支持体验。

Patterns

推荐模式

  • Create routes under
    app/
    with file names that map to URLs
  • Use
    _layout.tsx
    for shared layout, tabs, and stacks
  • Put platform-specific components in
    Component.ios.tsx
    and
    Component.android.tsx
  • Use
    expo config plugins
    in
    app.json
    /
    app.config.ts
    for any native config change
  • Use
    eas.json
    for EAS Build profiles
  • app/
    目录下创建与URL映射的文件名来定义路由
  • 使用
    _layout.tsx
    定义共享布局、标签栏和堆栈导航
  • 平台特定的组件放在
    Component.ios.tsx
    Component.android.tsx
    文件中
  • 任何原生配置修改都通过
    app.json
    /
    app.config.ts
    中的
    expo config plugins
    实现
  • 使用
    eas.json
    配置EAS Build构建配置

Anti-Patterns

反模式

  • Do not set up React Navigation manually in Expo projects
  • Do not use
    expo build
    (use EAS Build)
  • Do not edit
    ios/
    or
    android/
    directly in managed workflow
  • Do not use AsyncStorage for auth tokens or secrets
  • Do not use React Native's Image when expo-image is available
  • Do not use CodePush in Expo projects (use expo-updates)
  • 不要在Expo项目中手动配置React Navigation
  • 不要使用
    expo build
    (请使用EAS Build)
  • 不要在托管工作流中直接修改
    ios/
    android/
    目录下的文件
  • 不要使用AsyncStorage存储鉴权token或密钥
  • 当可以使用expo-image时不要使用React Native原生Image
  • 不要在Expo项目中使用CodePush(请使用expo-updates)