--- title: 'Tutorial: Creating a native view' sidebar_title: Create a native view description: A tutorial on creating a native view that renders a WebView with Expo modules API. --- import { Terminal } from '~/ui/components/Snippet'; import { Collapsible } from '~/ui/components/Collapsible'; In this tutorial, we are going to build a module with a native view that will render a WebView. We will be using the [WebView](https://developer.android.com/reference/android/webkit/WebView) component for Android and [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview) for iOS. It is possible to implement web support using [`iframe`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe), but we'll leave that as an exercise for the reader. ## 1. Initialize a new module First, we'll create a new module. On this page, we will use the name `expo-web-view`/`ExpoWebView`. You can name it whatever you like, just adjust the instructions accordingly: > **Tip**: Since you aren't going to actually ship this library, you can hit return for all of the prompts to accept the default values. ## 2. Set up our workspace Now let's clean up the default module a little bit so we have more of a clean slate and delete the code that we won't use in this guide. Find the following files and replace them with the provided minimal boilerplate: ```swift ios/ExpoWebViewModule.swift import ExpoModulesCore public class ExpoWebViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoWebView") View(ExpoWebView.self) {} } } ``` ```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt package expo.modules.webview import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoWebViewModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoWebView") View(ExpoWebView::class) {} } } ``` ```typescript src/index.ts export { default as WebView, Props as WebViewProps } from './ExpoWebView'; ``` ```typescript src/ExpoWebView.tsx import { ViewProps } from 'react-native'; import { requireNativeViewManager } from 'expo-modules-core'; import * as React from 'react'; export type Props = ViewProps; const NativeView: React.ComponentType = requireNativeViewManager('ExpoWebView'); export default function ExpoWebView(props: Props) { return ; } ``` ```typescript example/App.tsx import { WebView } from 'expo-web-view'; export default function App() { return ; } ``` ## 3. Run the example project Now let's run the example project to make sure everything is working. We'll need to start the TypeScript compiler to watch for changes and rebuild the module JavaScript, and separately in another terminal window we'll compile and run the example app. We should now see a blank purple screen. That's not very exciting. However, it's a good start. Let's make it a WebView now. ## 4. Add the system WebView as a subview Now we are going to add the system WebView with a hardcoded URL as a subview of our ExpoWebView. Our `ExpoWebView` class extends `ExpoView`, which extends `RCTView` from React Native, which finally extends `UIView` on iOS and `View` on Android. We need to ensure that the WebView subview has the same layout as ExpoWebView, whose layout will be calculated by React Native's layout engine. ### iOS view On iOS, we set `clipsToBounds` to `true` and set the `frame` of the WebView to the bounds of the ExpoWebView in `layoutSubviews` to match the layout. `init` is called when the view is created, and `layoutSubviews` is called when the layout changes. ```swift ios/ExpoWebView.swift import ExpoModulesCore import WebKit class ExpoWebView: ExpoView { let webView = WKWebView() required init(appContext: AppContext? = nil) { super.init(appContext: appContext) clipsToBounds = true addSubview(webView) let url = URL(string:"https://docs.expo.dev/modules/")! let urlRequest = URLRequest(url:url) webView.load(urlRequest) } override func layoutSubviews() { webView.frame = bounds } } ``` ### Android view On Android we use `LayoutParams` to set the layout of the WebView to match the layout of the ExpoWebView. We can do this when we instantiate the WebView. ```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt package expo.modules.webview import android.content.Context import android.webkit.WebView import android.webkit.WebViewClient import expo.modules.kotlin.AppContext import expo.modules.kotlin.views.ExpoView class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { internal val webView = WebView(context).also { it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) it.webViewClient = object : WebViewClient() {} addView(it) it.loadUrl("https://docs.expo.dev/modules/") } } ``` ### Example app No changes are needed, we can rebuild and run the app and you will see the [Expo Modules API overview page](/modules/). ## 5. Add a prop to set the URL To set a prop on our view, we'll need to define the prop name and setter inside of `ExpoWebViewModule`. In this case we're going to reach in and access `webView` property directly for convenience, but in many real world cases you will likely want to keep this logic inside of the `ExpoWebView` class and minimize the knowledge that `ExpoWebViewModule` has about the internals of `ExpoWebView`. We use the [Prop definition component](/modules/module-api/#prop) to define the prop. Within the prop setter block we can access the view and the prop. Note that we specify the url is of type `URL` — the Expo modules API will take care of converting strings to the native `URL` type for us. ### iOS module ```swift ios/ExpoWebViewModule.swift import ExpoModulesCore public class ExpoWebViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoWebView") View(ExpoWebView.self) { Prop("url") { (view, url: URL) in if view.webView.url != url { let urlRequest = URLRequest(url: url) view.webView.load(urlRequest) } } } } } ``` ### Android module ```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt package expo.modules.webview import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import java.net.URL class ExpoWebViewModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoWebView") View(ExpoWebView::class) { Prop("url") { view: ExpoWebView, url: URL? -> view.webView.loadUrl(url.toString()) } } } } ``` ### TypeScript module All we need to do here is add the `url` prop to the `Props` type. ```typescript src/ExpoWebView.tsx import { ViewProps } from 'react-native'; import { requireNativeViewManager } from 'expo-modules-core'; import * as React from 'react'; export type Props = { url?: string; } & ViewProps; const NativeView: React.ComponentType = requireNativeViewManager('ExpoWebView'); export default function ExpoWebView(props: Props) { return ; } ``` ### Example app Finally, we can pass in a URL to our WebView component in the example app. ```typescript example/App.tsx import { WebView } from 'expo-web-view'; export default function App() { return ; } ``` When you rebuild and run the app, you will now see the Expo homepage. ## 6. Add an event to notify when the page has loaded [View callbacks](/modules/module-api/#view-callbacks) allow developers to listen for events on components. They are typically registered through props on the component, for example: ``. We can use the [Events definition component](/modules/module-api/#events) to define an event for our WebView. We'll call it `onLoad` as well. ### iOS view and module On iOS, we need to implement `webView(_:didFinish:)` and make ExpoWebView extend `WKNavigationDelegate`. We can then call the `onLoad` from that delegate method. ```swift ios/ExpoWebView.swift import ExpoModulesCore import WebKit class ExpoWebView: ExpoView, WKNavigationDelegate { let webView = WKWebView() let onLoad = EventDispatcher() required init(appContext: AppContext? = nil) { super.init(appContext: appContext) clipsToBounds = true webView.navigationDelegate = self addSubview(webView) } override func layoutSubviews() { webView.frame = bounds } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { if let url = webView.url { onLoad([ "url": url.absoluteString ]) } } } ``` And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event. ```swift ios/ExpoWebViewModule.swift import ExpoModulesCore public class ExpoWebViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoWebView") View(ExpoWebView.self) { Events("onLoad") Prop("url") { (view, url: URL) in if view.webView.url != url { let urlRequest = URLRequest(url: url) view.webView.load(urlRequest) } } } } } ``` ### Android view and module On Android, we need to add override the `onPageFinished` function. We can then call the `onLoad` event handler that we defined in the module. ```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt package expo.modules.webview import android.content.Context import android.webkit.WebView import android.webkit.WebViewClient import expo.modules.kotlin.AppContext import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { private val onLoad by EventDispatcher() internal val webView = WebView(context).also { it.layoutParams = LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT ) it.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String) { onLoad(mapOf("url" to url)) } } addView(it) } } ``` And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event. ```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt package expo.modules.webview import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import java.net.URL class ExpoWebViewModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoWebView") View(ExpoWebView::class) { Events("onLoad") Prop("url") { view: ExpoWebView, url: URL? -> view.webView.loadUrl(url.toString()) } } } } ``` ### TypeScript module Note that event payloads are included within the `nativeEvent` property of the event, so to access the `url` from the `onLoad` event we would read `event.nativeEvent.url`. ```typescript src/ExpoWebView.tsx import { ViewProps } from 'react-native'; import { requireNativeViewManager } from 'expo-modules-core'; import * as React from 'react'; export type OnLoadEvent = { url: string; }; export type Props = { url?: string; onLoad?: (event: { nativeEvent: OnLoadEvent }) => void; } & ViewProps; const NativeView: React.ComponentType = requireNativeViewManager('ExpoWebView'); export default function ExpoWebView(props: Props) { return ; } ``` ### Example app Now we can update the example app to show an alert when the page has loaded. Copy in the following code, then rebuild and run your app, and you should see the alert! ```typescript example/App.tsx import { WebView } from 'expo-web-view'; export default function App() { return ( alert(`loaded ${event.nativeEvent.url}`)} /> ); } ``` ## 7. Bonus: Build a web browser UI around it Now that we have a web view, we can build a web browser UI around it. Have some fun trying to rebuild a browser UI, and maybe even add new native capabilities as needed (for example, to support a back or reload buttons). If you'd like some inspiration, there's a simple example below. ```typescript import { useState } from 'react'; import { ActivityIndicator, Platform, Text, TextInput, View } from 'react-native'; import { WebView } from 'expo-web-view'; import { StatusBar } from 'expo-status-bar'; export default function App() { const [inputUrl, setInputUrl] = useState('https://docs.expo.dev/modules/'); const [url, setUrl] = useState(inputUrl); const [isLoading, setIsLoading] = useState(true); return ( { if (inputUrl !== url) { setUrl(inputUrl); setIsLoading(true); } }} keyboardType="url" style={{ color: '#fff', backgroundColor: '#000', borderRadius: 10, marginHorizontal: 10, paddingHorizontal: 20, height: 60, }} /> setIsLoading(false)} style={{ flex: 1, marginTop: 20 }} /> ); } function LoadingView({ isLoading }: { isLoading: boolean }) { if (!isLoading) { return null; } return ( Loading... ); } ``` ![A simple web browser UI built around our WebView](/static/images/modules/native-view-tutorial/web-browser.png) ## Next steps Congratulations, you have created your first simple yet non-trivial Expo module with a native view for Android and iOS! Learn more about the API in the [Expo Module API reference](/modules/module-api/). If you enjoyed this tutorial and haven't done the native module tutorial, see [creating a native module](/modules/native-module-tutorial/) next.