在 Expo 原生应用中使用 React DOM

了解如何在 Expo 原生应用中使用 'use dom' 指令渲染 React DOM 组件。


信息SDK 52 及更高版本 可用。

Expo 提供了一种新方法,通过 'use dom' 指令直接在原生应用中使用现代网页代码。这使得可以通过按组件迁移的方式,实现整个网站向通用应用的渐进式迁移。

🌐 Expo offers a novel approach to work with modern web code directly in a native app via the 'use dom' directive. This enables incremental migration for an entire website to a universal app by moving on a per-component basis.

虽然 Expo 原生运行时通常不支持像 <div><img> 这样的元素,但有时你可能需要快速集成 Web 组件。在这种情况下,DOM 组件提供了一个有用的解决方案。

🌐 While the Expo native runtime generally does not support elements like <div> or <img>, there may be instances where you need to quickly incorporate web components. In such cases, DOM components provide a useful solution.

先决条件

🌐 Prerequisites

你的项目必须使用 Expo CLI 并扩展 Expo Metro 配置

如果你已经使用 npx expo [command] 运行你的项目(例如,如果你是用 npx create-expo-app 创建的),那么你就可以直接使用,无需执行此步骤。

🌐 If you already run your project with npx expo [command] (for example, if you created it with npx create-expo-app), then you're all set, and you can skip this step.

如果你的项目中还没有 expo 包,请通过运行以下命令来安装它,并选择使用 Expo CLI 和 Metro 配置

🌐 If you don't have the expo package in your project yet, then install it by running the command below and opt in to using Expo CLI and Metro Config:

Terminal
npx install-expo-modules@latest

如果命令失败,请参考安装 Expo 模块指南。

🌐 If the command fails, refer to the Installing Expo modules guide.

Expo Metro 运行时、React DOM 和 React Native Web

如果你正在使用 Expo Router 和 Expo Web,可以跳过此步骤。否则,请安装以下软件包:

🌐 If you are using Expo Router and Expo Web, you can skip this step. Otherwise, install the following packages:

Terminal
npx expo install @expo/metro-runtime react-dom react-native-web

用法

🌐 Usage

在你的项目中安装 react-native-webview

🌐 Install react-native-webview in your project:

Terminal
npx expo install react-native-webview

要将 React 组件渲染到 DOM,请在 web 组件文件的顶部添加 'use dom' 指令:

🌐 To render a React component to the DOM, add the 'use dom' directive to the top of the web component file:

my-component.tsx (web)
'use dom'; export default function DOMComponent({ name }: { name: string }) { return ( <div> <h1>Hello, {name}</h1> </div> ); }

在本地组件文件中,导入 web 组件以使用它:

🌐 Inside the native component file, import the web component to use it:

App.tsx (native)
import DOMComponent from './my-component.tsx'; export default function App() { return ( // This is a DOM component. It re-exports a wrapped `react-native-webview` behind the scenes. <DOMComponent name="Europa" /> ); }

特性

🌐 Features

  • 在 Web、原生和 DOM 组件之间共享打包器配置。
  • 在 DOM 组件中已启用 React、TypeScript、CSS 以及所有其他 Metro 功能。
  • 在终端和 Safari/Chrome 中进行调试日志记录。
  • 快速刷新和热模块替换。
  • 用于离线支持的嵌入式导出。
  • 资源在网页和原生应用中实现统一。
  • DOM 组件包可以在 Expo Atlas 中进行检查以进行调试。
  • 无需本地重建即可访问所有网络功能。
  • 开发中运行时错误覆盖
  • 支持 Expo Go。

WebView 属性

🌐 WebView props

要将属性传递给底层的原生 WebView,请在组件上使用 dom 属性。该属性内置于每个 DOM 组件中,并接受一个对象,其中包含你希望更改的任何 WebView 属性

🌐 To pass props to the underlying native WebView, use the dom prop on the component. This prop is built into every DOM component and accepts an object with any WebView props that you would like to change.

App.tsx (native)
import DOMComponent from './my-component'; export default function App() { return ( <DOMComponent dom={{ scrollEnabled: false, }} /> ); }

在你的 DOM 组件上,添加 dom 属性,这样 TypeScript 就能识别它:

🌐 On your DOM component, add the dom prop so it is recognized in TypeScript:

my-component.tsx (web)
'use dom'; export default function DOMComponent({}: { dom: import('expo/dom').DOMProps }) { return ( <div> <h1>Hello, world!</h1> </div> ); }

编组属性

🌐 Marshalled props

你可以通过可序列化属性(numberstringbooleannullundefinedArrayObject)向 DOM 组件发送数据。例如,在原生组件文件中,你可以向 DOM 组件传递一个属性:

🌐 You can send data to the DOM component through serializable props (number, string, boolean, null, undefined, Array, Object). For example, inside a native component file, you can pass a prop to the DOM component:

App.tsx (native)
import DOMComponent from './my-component'; export default function App() { return <DOMComponent hello={'world'} />; }

在 web 组件文件中,你可以像下面的示例一样接收 prop:

🌐 Inside the web component file, you can receive the prop as shown in the example below:

my-component.tsx (web)
'use dom'; export default function DOMComponent({ hello }: { hello: string }) { return <p>Hello, {hello}</p>; }

属性通过异步桥传递,因此它们不会同步更新。它们作为属性传递给 React 根组件,这意味着它们会重新渲染整个 React 树。

🌐 Props are sent over an asynchronous bridge so they are not updated synchronously. They are passed as props to the React root component, which means they re-render the entire React tree.

本地操作

🌐 Native actions

你可以通过将异步函数作为顶层属性传递给 DOM 组件,将类型安全的原生函数发送到 DOM 组件:

🌐 You can send type-safe native functions to DOM components by passing asynchronous functions as top-level props to the DOM component:

App.tsx (native)
import DomComponent from './my-component'; export default function App() { return ( <DomComponent hello={(data: string) => { console.log('Hello', data); }} /> ); }
my-component.tsx (web)
'use dom'; export default function MyComponent({ hello }: { hello: (data: string) => Promise<void> }) { return <p onClick={() => hello('world')}>Click me</p>; }

你不能将函数作为嵌套属性传递给 DOM 组件。它们必须是顶层属性。

原生操作总是异步的,并且只接受可序列化的参数(意味着不能是函数),因为数据会通过桥接发送到 DOM 组件的 JavaScript 引擎。

🌐 Native actions are always asynchronous and accept only serializable arguments (meaning no functions) because the data is sent over a bridge to the DOM component's JavaScript engine.

原生操作可以向 DOM 组件返回可序列化的数据,这对于从原生端获取数据非常有用。

🌐 Native actions can return serializable data to the DOM component, which is useful for getting data back from the native side.

getDeviceName(): Promise<string> { return DeviceInfo.getDeviceName(); }

把这些函数想象成 React 服务器函数,但它们不是驻留在服务器上,而是本地存在于原生应用中,并与 DOM 组件通信。这种方法为你的 DOM 组件提供了一种强大的方式来添加真正的原生功能。

🌐 Think of these functions like React Server Functions, but instead of residing on the server, they live locally in the native app and communicate with the DOM component. This approach provides a powerful way to add truly native functionality to your DOM components.

传递引用

🌐 Passing refs

重要 这是测试版,将来可能会有所更改。

你可以在 DOM 组件中使用 useDOMImperativeHandle 钩子来接受来自原生端的 ref 调用。这个钩子类似于 React 的 useImperativeHandle 钩子,但它不需要传入 ref 对象。

🌐 You can use the useDOMImperativeHandle hook inside a DOM component to accept ref calls from the native side. This hook is similar to React's useImperativeHandle hook, but it does not need a ref object to be passed to it.

App.tsx (native)
import { useRef } from 'react'; import { Button, View } from 'react-native'; import MyComponent, { type DOMRef } from './my-component'; export default function App() { const ref = useRef<DOMRef>(null); return ( <View style={{ flex: 1 }}> <MyComponent ref={ref} /> <Button title="focus" onPress={() => { ref.current?.focus(); }} /> </View> ); }

Expo SDK 53 及更高版本使用 React 19。这意味着 ref 属性会作为 prop 传递给组件,并且你可以在组件中直接使用它。

🌐 Expo SDK 53 and later use React 19. This means that the ref prop is passed to the component as a prop, and you can use it directly in the component.

my-component.tsx (web)
'use dom'; import { useDOMImperativeHandle, type DOMImperativeFactory } from 'expo/dom'; import { Ref, useRef } from 'react'; export interface DOMRef extends DOMImperativeFactory { focus: () => void; } export default function MyComponent(props: { ref: Ref<DOMRef>; dom?: import('expo/dom').DOMProps; }) { const inputRef = useRef<HTMLInputElement>(null); useDOMImperativeHandle( props.ref, () => ({ focus: () => { inputRef.current?.focus(); }, }), [] ); return <input ref={inputRef} />; }

在 Expo SDK 52 及更早版本(React 18)中,请使用传统的 forwardRef 函数来访问 ref 句柄。

🌐 In Expo SDK 52 and earlier (React 18), use the legacy forwardRef function to access the ref handle.

my-component.tsx (web)
'use dom'; import { useDOMImperativeHandle, type DOMImperativeFactory } from 'expo/dom'; import { forwardRef, useRef } from 'react'; export interface MyRef extends DOMImperativeFactory { focus: () => void; } export default forwardRef<MyRef, object>(function MyComponent(props, ref) { const inputRef = useRef<HTMLInputElement>(null); useDOMImperativeHandle( ref, () => ({ focus: () => { inputRef.current?.focus(); }, }), [] ); return <input ref={inputRef} />; });

React 的设计是单向数据流,所以使用回调向上传递数据的概念并不是惯用做法。这样的行为可能不稳定,并且在未来的 React 新版本中可能会被废弃。将数据传回上层组件的推荐方式是使用原生操作,它们会更新状态,然后将状态传回 DOM 组件。

🌐 React is meant to have a unilateral data flow, so the concept of using callbacks to go back up the tree is not idiomatic. Expect the behavior to be flakey and possibly phased out in the future with newer versions of React. The preferred way to send data back up the tree is to use native actions, which update the state and then pass it back to the DOM component.

特性检测

🌐 Feature detection

由于 DOM 组件用于运行网站,你可能需要额外的限定符来更好地支持某些库。你可以使用以下代码来检测组件是否在 DOM 组件中运行:

🌐 Since DOM components are used to run websites, you might need extra qualifiers to better support certain libraries. You can detect if a component is running in a DOM component with the following code:

import { IS_DOM } from 'expo/dom';

虽然 process.env.EXPO_OS 在 DOM 组件中总是网页,但你可以使用 process.env.EXPO_DOM_HOST_OS 检测 顶层 平台。根据顶层原生平台的操作系统,这将是 iosandroid,而网页上则为 undefined

🌐 While process.env.EXPO_OS will always be web in a DOM component, you can detect the top platform with process.env.EXPO_DOM_HOST_OS. This will be either ios, android, depending on the OS of the top-most native platform, and undefined on web.

公共资源

🌐 Public assets

重要 警告: 这是 alpha 版本,未来可能会发生变化。EAS 更新不支持公共资源。请使用 require() 加载本地资源。

public 目录的内容会被复制到原生应用的二进制文件中,以支持在 DOM 组件中使用公共资源。由于这些公共资源将从本地文件系统提供,请使用 process.env.EXPO_BASE_URL 前缀来引用正确的路径。例如:

🌐 The contents of the root public directory are copied to the native app's binary to support the use of public assets in DOM components. Since these public assets will be served from the local filesystem, use the process.env.EXPO_BASE_URL prefix to reference the correct path. For example:

<img src={`${process.env.EXPO_BASE_URL}img.png`} />

调试

🌐 Debugging

默认情况下,所有 console.log 方法在 WebView 中都会被扩展,以将日志转发到终端。这使得查看 DOM 组件中的情况变得快速且容易。

🌐 By default, all console.log methods are extended in WebViews to forward logs to the terminal. This makes it fast and easy to see what's happening in your DOM components.

Expo 还支持在开发模式下打包时对 WebView 进行检查和调试。你可以打开 Safari > 开发 > 模拟器 > MyComponent.tsx 来查看 WebView 的控制台并检查元素。

🌐 Expo also enables WebView inspection and debugging when bundling in development mode. You can open Safari > Develop > Simulator > MyComponent.tsx to see the WebView's console and inspect elements.

手动 WebView

🌐 Manual WebViews

你可以使用 react-native-webview 中的 WebView 组件手动创建 WebView。这对于渲染来自远程服务器的网站非常有用。

🌐 You can create a manual WebView using the WebView component from react-native-webview. This can be useful for rendering websites from a remote server.

App.tsx (native)
import { WebView } from 'react-native-webview'; export default function App() { return <WebView source={{ html: '<h1>Hello, world!</h1>' }} />; }

路由

🌐 Routing

Expo Router API,例如 <Link />useRouter,可以在 DOM 组件中使用来在路由之间导航。

🌐 Expo Router APIs such as <Link />, and useRouter can be used in DOM components to navigate between routes.

my-component.tsx (web)
'use dom'; import Link from 'expo-router/link'; export default function DOMComponent() { return ( <div> <h1>Hello, world!</h1> <Link href="/about">About</Link> </div> ); }

同步返回路由信息的 API,例如 useLocalSearchParams()useGlobalSearchParams()usePathname()useSegments()useRootNavigation()useRootNavigationState(),不受自动支持。相反,应在 DOM 组件之外读取这些值,并将它们作为 props 提供。

🌐 APIs that synchronously return routing info such as useLocalSearchParams(), useGlobalSearchParams(), usePathname(), useSegments(), useRootNavigation(), and useRootNavigationState() are not automatically supported. Instead, read these values outside of DOM components and supply them as props.

App.tsx (native)
import DOMComponent from './my-component'; import { usePathname } from 'expo-router'; export default function App() { const pathname = usePathname(); return <DOMComponent pathname={pathname} />; }

router.canGoBack()router.canDismiss() 函数也不受支持,需要手动处理,这可以确保不会触发额外的渲染周期。

🌐 The router.canGoBack() and router.canDismiss() functions are also unsupported and require manual marshalling, this ensures no extraneous render cycles are triggered.

避免使用标准的网页 <a /> 锚点元素进行导航,因为这些会改变 DOM 组件的来源,使用户可能无法返回。若想展示外部网站,建议使用 WebBrowser

🌐 Avoid using standard web <a /> anchor elements for navigation as these will change the DOM component origin in a way that users may not be able to navigate back from. Prefer launching WebBrowsers if you want to present external websites.

由于 DOM 组件无法渲染原生子组件,布局路由(_layout)永远不能是 DOM 组件。你可以从布局路由渲染 DOM 组件来创建标题、背景等,但布局路由本身应始终是原生组件。

🌐 Since DOM components cannot render native children, layout routes (_layout) can never be DOM components. You can render DOM components from layout routes to create headers, backgrounds, and more, but the layout route itself should always be native.

测量 DOM 组件

🌐 Measuring DOM components

你可能想要测量一个 DOM 组件的大小并将其反馈给原生端(例如,原生滚动)。这可以通过使用 matchContents 属性或手动的原生操作来完成:

🌐 You may want to measure the size of a DOM component and report it back to the native side (for example, native scrolling). This can be done using a matchContents prop or a manual native action:

自动使用 matchContents 属性

🌐 Automatically with matchContents prop

你可以使用 dom={{ matchContents: true }} 属性来自动测量 DOM 组件的大小并调整原生视图的尺寸。这在某些布局中特别有用,例如当组件必须具有固有尺寸才能显示时,比如组件位于父视图的中心时:

🌐 You can use the dom={{ matchContents: true }} prop to measure the size of the DOM component automatically and resize the native view. This is particularly useful for certain layouts where the DOM component must have an intrinsic size in order to be displayed, such as when the component is centered within a parent view:

App.tsx (native)
import DOMComponent from './my-component'; export default function Route() { return <DOMComponent dom={{ matchContents: true }} />; }

通过手动指定大小

🌐 Manually by specifying a size

你也可以通过 dom 属性将其传递给 WebView style 属性来手动指定大小:

🌐 You can also manually provide a size by passing it to the WebView style prop via the dom prop:

App.tsx (native)
import DOMComponent from './my-component'; export default function Route() { return ( <DOMComponent dom={{ style: { width, height }, }} /> ); }

观察尺寸变化

🌐 Observing changes in size

如果你希望将 DOM 组件大小的变化报告回原生端,可以向你的 DOM 组件添加一个原生动作,该动作会在大小发生变化时调用:

🌐 If you would like to report changes in the size of the DOM component back to the native side, you can add a native action to your DOM component that is called whenever the size is changed:

my-component.tsx (web)
'use dom'; import { useEffect } from 'react'; function useSize(callback: (size: { width: number; height: number }) => void) { useEffect(() => { // Observe window size changes const observer = new ResizeObserver(entries => { for (const entry of entries) { const { width, height } = entry.contentRect; callback({ width, height }); } }); observer.observe(document.body); callback({ width: document.body.clientWidth, height: document.body.clientHeight, }); return () => { observer.disconnect(); }; }, [callback]); } export default function DOMComponent({ onDOMLayout, }: { dom?: import('expo/dom').DOMProps; onDOMLayout: (size: { width: number; height: number }) => void; }) { useSize(onDOMLayout); return <div style={{ width: 500, height: 500, background: 'blue' }} />; }

然后更新你的原生代码,在 DOM 组件报告尺寸变化时,将尺寸设置到状态中:

🌐 Then update your native code to set the size in state whenever the DOM component reports a change in size:

App.tsx (native)
import DOMComponent from '@/components/my-component'; import { useState } from 'react'; import { View, ScrollView } from 'react-native'; export default function App() { const [containerSize, setContainerSize] = useState<{ width: number; height: number; } | null>(null); return ( <View style={{ flex: 1 }}> <ScrollView> <DOMComponent onDOMLayout={async ({ width, height }) => { if (containerSize?.width !== width || containerSize?.height !== height) { setContainerSize({ width, height }); } }} dom={{ containerStyle: containerSize != null ? { width: containerSize.width, height: containerSize.height } : null, }} /> </ScrollView> </View> ); }

架构

🌐 Architecture

内置的 DOM 支持仅能将网站呈现为单页应用(不支持 SSR 或 SSG)。这是因为嵌入的 JS 代码不需要进行搜索引擎优化和索引。

🌐 Built-in DOM support only renders websites as single-page applications (no SSR or SSG). This is because search engine optimization and indexing are unnecessary for embedded JS code.

当一个模块被标记为 'use dom' 时,它会被在运行时导入的代理引用所替代。这个功能主要是通过一系列打包工具和命令行技术实现的。

🌐 When a module is marked with 'use dom', it is replaced with a proxy reference imported at runtime. This feature is primarily achieved through a series of bundler and CLI techniques.

如果需要,你仍然可以通过将原始 HTML 传递给 WebView 组件来使用标准方式的 WebView。

🌐 If desired, you can still use a WebView with the standard approach by passing raw HTML to a WebView component.

在网站或其他 DOM 组件中渲染的 DOM 组件将表现为常规组件,并且 dom 属性将被忽略。这是因为网页内容是直接传递的,而不是被封装在 iframe 中。

🌐 DOM components rendered within websites or other DOM components will behave as regular components, and the dom prop will be ignored. This is because web content is passed directly through and not wrapped in an iframe.

总体而言,该系统与 Expo 的 React 服务器组件实现有许多相似之处。

🌐 Overall, this system shares many similarities with Expo's React Server Components implementation.

注意事项

🌐 Considerations

我们建议使用通用原语如 ViewImageText 来构建真正的原生应用。DOM 组件仅支持标准的 JavaScript,其解析和启动速度比优化后的 Hermes 字节码要慢。

🌐 We recommend building truly native apps using universal primitives such as View, Image, and Text. DOM components only support standard JavaScript, which is slower to parse and start up than optimized Hermes bytecode.

数据只能通过异步 JSON 传输系统在 DOM 组件和原生组件之间传递。避免依赖跨 JS 引擎的数据和在 DOM 组件中深度链接到嵌套 URL,因为它们目前不支持与 Expo Router 的完整协调。

🌐 Data can be sent between DOM components and native components only through an asynchronous JSON transport system. Avoid relying on data across JS engines and deep linking to nested URLs in DOM components, as they do not currently support full reconciliation with Expo Router.

虽然 DOM 组件并非 Expo Router 独有,但它们是针对 Expo Router 应用开发和测试的,以在与 Expo Router 一起使用时提供最佳体验。

🌐 While DOM components are not exclusive to Expo Router, they are developed and tested against Expo Router apps to provide the best experience when used with Expo Router.

如果你有一个用于共享数据的全局状态,它将无法跨 JavaScript 引擎访问。

🌐 If you have a global state for sharing data, it will not be accessible across JS engines.

虽然 Expo SDK 中的原生模块可以优化以支持 DOM 组件,但这种优化尚未实现。请使用原生操作和属性将原生功能与 DOM 组件共享。

🌐 While native modules in the Expo SDK can be optimized to support DOM components, this optimization has not been implemented yet. Use native actions and props to share native functionality with DOM components.

DOM 组件和网站整体上不如原生视图高效,但它们仍有一些合理的使用场景。例如,从概念上来说,网页是呈现富文本和 Markdown 的最佳方式。网页还拥有非常优秀的 WebGL 支持,但需要注意的是,处于低功耗模式的设备通常会降低网页帧率以节省电量。

🌐 DOM components and websites in general are less optimal than native views but there are some reasonable uses for them. For example, the web is conceptually the best way to render rich-text and markdown. The web also has very good WebGL support, with the caveat that devices in low-power mode will often throttle web frame rates to preserve battery.

许多大型应用也会使用一些网页内容作为辅助路由,例如博客文章、富文本(例如 X 上的长篇文章)、设置页面、帮助页面以及应用中其他访问频率较低的部分。

🌐 Many large apps also use some web content for auxiliary routes such as blog posts, rich-text (for example, long-form posts on X), settings pages, help pages, and other less frequently visited parts of the app.

设置提供程序

🌐 Server Components

DOM 组件目前仅以单页应用的形式渲染,不支持静态渲染或 React 服务器组件(RSC)。当项目使用 React 服务器组件时,无论平台如何,'use dom' 的作用都与 'use client' 相同。RSC 负载可以作为属性传递给 DOM 组件。然而,它们在原生平台上无法正确水化,因为它们将为原生运行时进行渲染。

🌐 DOM components currently only render as single-page applications and don't support static rendering or React Server Components (RSC). When the project uses React Server Components, 'use dom' will work the same as 'use client' regardless of the platform. RSC Payloads can be passed as properties to DOM components. However, they cannot be hydrated correctly on native platforms as they'll be rendered for a native runtime.

局限性

🌐 Limitations

  • 与服务器组件不同,你不能将 children 传递给 DOM 组件。
  • DOM 组件是独立的,不会在不同实例之间自动共享数据。
  • 你不能将原生视图添加到 DOM 组件中。虽然你可以尝试将原生视图悬浮在 DOM 组件上方,但这种方法会导致较差的用户体验。
  • 函数属性不能同步返回值。它们必须是异步的。
  • DOM 组件目前只能嵌入,且不支持 OTA 更新。未来可能会作为 React 服务器组件的一部分添加此功能。

归根结底,通用架构是最令人兴奋的类型。Expo CLI 广泛的通用工具链是我们能够提供如此复杂且有价值的功能的唯一原因。

🌐 Ultimately, universal architecture is the most exciting kind. Expo CLI's extensive universal tooling is the only reason we can even offer a feature as intricate and valuable as this one.

虽然 DOM 组件有助于迁移和快速开发,但我们建议尽可能使用真正的原生视图。

🌐 While DOM components help with migration and moving quickly, we recommend using truly native views whenever possible.

常见问题

🌐 Common questions

如何在 DOM 组件中获取安全上下文?

🌐 How to obtain a Secure Context in DOM components?

一些 Web API 需要在安全上下文中才能正常工作。例如,剪贴板 API 仅在安全上下文中可用。安全上下文意味着远程资源必须通过 HTTPS 提供。了解更多关于仅限安全上下文的功能

🌐 Some Web APIs require a Secure Context function correctly. For example, the Clipboard API is only available in secure contexts. A secure context means that remote resources must be served over HTTPS. Learn more about features restricted to secure contexts.

为了确保你的 DOM 组件在安全的环境中运行,请遵循以下指南:

🌐 To ensure your DOM components run within a secure context, follow these guidelines:

  • 发布版本:使用 file:// 方案提供的 DOM 组件默认具有安全上下文。
  • 调试构建:在使用开发服务器(默认使用 http:// 协议)时,你可以使用 隧道 来通过 HTTPS 提供 DOM 组件。
通过 HTTPS 隧道传输 DOM 组件的示例命令
Terminal
# Install expo-dev-client to enable connection to the remote development server:
npx expo install expo-dev-client

# Run the app on Android:
npx expo run:android
# Press Ctrl + C to stop the server
npx expo start --tunnel -d -a

# Run the app on iOS:
npx expo run:ios
# Press Ctrl + C to stop the server
npx expo start --tunnel -d -i