1---
2title: 'Tutorial: Creating a native view'
3sidebar_title: Creating a native view
4---
5
6import { CodeBlocksTable } from '~/components/plugins/CodeBlocksTable';
7import { PlatformTag } from '~/ui/components/Tag';
8import { APIBox } from '~/components/plugins/APIBox';
9import { Terminal } from '~/ui/components/Snippet';
10import { Collapsible } from '~/ui/components/Collapsible';
11
12In 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.
13
14## 1. Initialize a new module
15
16First, 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:
17
18<Terminal cmd={['$ npx create-expo-module expo-web-view']} />
19
20> **Tip**: Since you aren't going to actually ship this library, you can hit <kbd>return</kbd> for all of the prompts to accept the default values.
21
22## 2. Set up our workspace
23
24Now 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.
25
26<Terminal
27  cmdCopy="cd expo-web-view && rm src/ExpoWebView.types.ts src/ExpoWebView.web.tsx src/ExpoWebViewModule.ts src/ExpoWebViewModule.web.ts"
28  cmd={[
29    '$ cd expo-web-view',
30    '$ rm src/ExpoWebView.types.ts src/ExpoWebViewModule.ts',
31    '$ rm src/ExpoWebView.web.tsx src/ExpoWebViewModule.web.ts',
32  ]}
33/>
34
35Find the following files and replace them with the provided minimal boilerplate:
36
37```swift ios/ExpoWebViewModule.swift
38import ExpoModulesCore
39
40public class ExpoWebViewModule: Module {
41  public func definition() -> ModuleDefinition {
42    Name("ExpoWebView")
43
44    View(ExpoWebView.self) {}
45  }
46}
47```
48
49```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
50package expo.modules.webview
51
52import expo.modules.kotlin.modules.Module
53import expo.modules.kotlin.modules.ModuleDefinition
54
55class ExpoWebViewModule : Module() {
56  override fun definition() = ModuleDefinition {
57    Name("ExpoWebView")
58
59    View(ExpoWebView::class) {}
60  }
61}
62```
63
64```typescript src/index.ts
65export { default as WebView, Props as WebViewProps } from './ExpoWebView';
66```
67
68```typescript src/ExpoWebView.tsx
69import { ViewProps } from 'react-native';
70import { requireNativeViewManager } from 'expo-modules-core';
71import * as React from 'react';
72
73export type Props = ViewProps;
74
75const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
76
77export default function ExpoWebView(props: Props) {
78  return <NativeView {...props} />;
79}
80```
81
82```typescript example/App.tsx
83import { WebView } from 'expo-web-view';
84
85export default function App() {
86  return <WebView style={{ flex: 1, backgroundColor: 'purple' }} />;
87}
88```
89
90## 3. Run the example project
91
92Now 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.
93
94<Terminal
95  cmdCopy="npm run build"
96  cmd={[
97    '# Run this in the root of the project to start the TypeScript compiler',
98    '$ npm run build',
99  ]}
100/>
101
102<Terminal
103  cmdCopy="cd example && npx expo run:ios"
104  cmd={[
105    '$ cd example',
106    '# Run the example app on iOS',
107    '$ npx expo run:ios',
108    '# Run the example app on Android',
109    '$ npx expo run:android',
110  ]}
111/>
112
113We should now see... a blank purple screen. That's not very exciting, but it's a good start. Let's actually make it a WebView now.
114
115## 4. Add the system WebView as a subview
116
117Now 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.
118
119### iOS view
120
121On 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.
122
123```swift ios/ExpoWebView.swift
124import ExpoModulesCore
125import WebKit
126
127class ExpoWebView: ExpoView {
128  let webView = WKWebView()
129
130  required init(appContext: AppContext? = nil) {
131    super.init(appContext: appContext)
132    clipsToBounds = true
133    addSubview(webView)
134
135    let url =  URL(string:"https://docs.expo.dev/modules/")!
136    let urlRequest = URLRequest(url:url)
137    webView.load(urlRequest)
138  }
139
140  override func layoutSubviews() {
141    webView.frame = bounds
142  }
143}
144```
145
146### Android view
147
148On 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.
149
150```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt
151package expo.modules.webview
152
153import android.content.Context
154import android.webkit.WebView
155import android.webkit.WebViewClient
156import expo.modules.kotlin.AppContext
157import expo.modules.kotlin.views.ExpoView
158
159class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
160  internal val webView = WebView(context).also {
161    it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
162    it.webViewClient = object : WebViewClient() {}
163    addView(it)
164
165    it.loadUrl("https://docs.expo.dev/modules/")
166  }
167}
168```
169
170### Example app
171
172No changes are needed, we can rebuild and run the app and you will see the [Expo Modules API overview page](/modules/).
173
174## 5. Add a prop to set the URL
175
176To 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`.
177
178We 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.
179
180### iOS module
181
182
183```swift ios/ExpoWebViewModule.swift
184import ExpoModulesCore
185
186public class ExpoWebViewModule: Module {
187  public func definition() -> ModuleDefinition {
188    Name("ExpoWebView")
189
190    View(ExpoWebView.self) {
191      Prop("url") { (view, url: URL) in
192        if view.webView.url != url {
193          let urlRequest = URLRequest(url: url)
194          view.webView.load(urlRequest)
195        }
196      }
197    }
198  }
199}
200```
201
202### Android module
203
204```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
205package expo.modules.webview
206
207import expo.modules.kotlin.modules.Module
208import expo.modules.kotlin.modules.ModuleDefinition
209import java.net.URL
210
211class ExpoWebViewModule : Module() {
212  override fun definition() = ModuleDefinition {
213    Name("ExpoWebView")
214
215    View(ExpoWebView::class) {
216      Prop("url") { view: ExpoWebView, url: URL? ->
217        view.webView.loadUrl(url.toString())
218      }
219    }
220  }
221}
222```
223
224### TypeScript module
225
226All we need to do here is add the `url` prop to the `Props` type.
227
228```typescript src/ExpoWebView.tsx
229import { ViewProps } from 'react-native';
230import { requireNativeViewManager } from 'expo-modules-core';
231import * as React from 'react';
232
233export type Props = {
234  url?: string;
235} & ViewProps;
236
237const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
238
239export default function ExpoWebView(props: Props) {
240  return <NativeView {...props} />;
241}
242```
243
244### Example app
245
246Finally, we can pass in a URL to our WebView component in the example app.
247
248```typescript example/App.tsx
249import { WebView } from 'expo-web-view';
250
251export default function App() {
252  return <WebView style={{ flex: 1 }} url="https://expo.dev" />;
253}
254```
255
256When you rebuild and run the app, you will now see the Expo homepage.
257
258## 6. Add an event to notify when the page has loaded
259
260[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: `<Image onLoad={...} />`.  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.
261
262### iOS view and module
263
264On iOS, we need to implement `webView(_:didFinish:)` and make ExpoWebView extend `WKNavigationDelegate`. We can then call the `onLoad` from that delegate method.
265
266```swift ios/ExpoWebView.swift
267import ExpoModulesCore
268import WebKit
269
270class ExpoWebView: ExpoView, WKNavigationDelegate {
271  let webView = WKWebView()
272  let onLoad = EventDispatcher()
273
274  required init(appContext: AppContext? = nil) {
275    super.init(appContext: appContext)
276    clipsToBounds = true
277    webView.navigationDelegate = self
278    addSubview(webView)
279  }
280
281  override func layoutSubviews() {
282    webView.frame = bounds
283  }
284
285  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
286    if let url = webView.url {
287      onLoad([
288        "url": url.absoluteString
289      ])
290    }
291  }
292}
293```
294
295And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event.
296
297```swift ios/ExpoWebViewModule.swift
298import ExpoModulesCore
299
300public class ExpoWebViewModule: Module {
301  public func definition() -> ModuleDefinition {
302    Name("ExpoWebView")
303
304    View(ExpoWebView.self) {
305      Events("onLoad")
306
307      Prop("url") { (view, url: URL) in
308        if view.webView.url != url {
309          let urlRequest = URLRequest(url: url)
310          view.webView.load(urlRequest)
311        }
312      }
313    }
314  }
315}
316```
317
318### Android view and module
319
320On Android, we need to add override the `onPageFinished` function. We can then call the `onLoad` event handler that we defined in the module.
321
322```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt
323package expo.modules.webview
324
325import android.content.Context
326import android.webkit.WebView
327import android.webkit.WebViewClient
328import expo.modules.kotlin.AppContext
329import expo.modules.kotlin.viewevent.EventDispatcher
330import expo.modules.kotlin.views.ExpoView
331
332class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
333  private val onLoad by EventDispatcher()
334
335  internal val webView = WebView(context).also {
336    it.layoutParams = LayoutParams(
337      LayoutParams.MATCH_PARENT,
338      LayoutParams.MATCH_PARENT
339    )
340
341    it.webViewClient = object : WebViewClient() {
342      override fun onPageFinished(view: WebView, url: String) {
343        onLoad(mapOf("url" to url))
344      }
345    }
346
347    addView(it)
348  }
349}
350```
351
352And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event.
353
354```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
355package expo.modules.webview
356
357import expo.modules.kotlin.modules.Module
358import expo.modules.kotlin.modules.ModuleDefinition
359import java.net.URL
360
361class ExpoWebViewModule : Module() {
362  override fun definition() = ModuleDefinition {
363    Name("ExpoWebView")
364
365    View(ExpoWebView::class) {
366      Events("onLoad")
367
368      Prop("url") { view: ExpoWebView, url: URL? ->
369        view.webView.loadUrl(url.toString())
370      }
371    }
372  }
373}
374```
375
376### TypeScript module
377
378Note 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`.
379
380```typescript src/ExpoWebView.tsx
381import { ViewProps } from 'react-native';
382import { requireNativeViewManager } from 'expo-modules-core';
383import * as React from 'react';
384
385export type OnLoadEvent = {
386  url: string;
387};
388
389export type Props = {
390  url?: string;
391  onLoad?: (event: { nativeEvent: OnLoadEvent }) => void;
392} & ViewProps;
393
394const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
395
396export default function ExpoWebView(props: Props) {
397  return <NativeView {...props} />;
398}
399```
400
401### Example app
402
403Now 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!
404
405```typescript example/App.tsx
406import { WebView } from 'expo-web-view';
407
408export default function App() {
409  return (
410    <WebView
411      style={{ flex: 1 }}
412      url="https://expo.dev"
413      onLoad={event => alert(`loaded ${event.nativeEvent.url}`)}
414    />
415  );
416}
417```
418
419## 7. Bonus: Build a web browser UI around it
420
421Now 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.
422
423<Collapsible summary="example/App.tsx">
424
425```typescript
426import { useState } from "react";
427import {
428  ActivityIndicator,
429  Platform,
430  Text,
431  TextInput,
432  View,
433} from "react-native";
434import { WebView } from "expo-web-view";
435import { StatusBar } from "expo-status-bar";
436
437export default function App() {
438  const [inputUrl, setInputUrl] = useState("https://docs.expo.dev/modules/");
439  const [url, setUrl] = useState(inputUrl);
440  const [isLoading, setIsLoading] = useState(true);
441
442  return (
443    <View style={{ flex: 1, paddingTop: Platform.OS === "ios" ? 80 : 30 }}>
444      <TextInput
445        value={inputUrl}
446        onChangeText={setInputUrl}
447        returnKeyType="go"
448        autoCapitalize="none"
449        onSubmitEditing={() => {
450          if (inputUrl !== url) {
451            setUrl(inputUrl);
452            setIsLoading(true);
453          }
454        }}
455        keyboardType="url"
456        style={{
457          color: "#fff",
458          backgroundColor: "#000",
459          borderRadius: 10,
460          marginHorizontal: 10,
461          paddingHorizontal: 20,
462          height: 60,
463        }}
464      />
465
466      <WebView
467        url={
468          url.startsWith("https://") || url.startsWith("http://")
469            ? url
470            : `https://${url}`
471        }
472        onLoad={() => setIsLoading(false)}
473        style={{ flex: 1, marginTop: 20 }}
474      />
475      <LoadingView isLoading={isLoading} />
476      <StatusBar style="auto" />
477    </View>
478  );
479}
480
481function LoadingView({ isLoading }: { isLoading: boolean }) {
482  if (!isLoading) {
483    return null;
484  }
485
486  return (
487    <View
488      style={{
489        position: "absolute",
490        bottom: 0,
491        left: 0,
492        right: 0,
493        height: 80,
494        backgroundColor: "rgba(0,0,0,0.5)",
495        paddingBottom: 10,
496        justifyContent: "center",
497        alignItems: "center",
498        flexDirection: "row",
499      }}
500    >
501      <ActivityIndicator
502        animating={isLoading}
503        color="#fff"
504        style={{ marginRight: 10 }}
505      />
506      <Text style={{ color: "#fff" }}>Loading...</Text>
507    </View>
508  );
509}
510
511```
512
513</Collapsible>
514
515![A simple web browser UI built around our WebView](/static/images/modules/native-view-tutorial/web-browser.png)
516
517## Next steps
518
519Congratulations, you have created your first simple yet non-trivial Expo module with a native view for iOS and Android! Learn more about the API in the [Expo Module API reference](/modules/module-api/).
520
521if you enjoyed this tutorial and haven't done the native module tutorial, go to the ["creating a native module" tutorial](/modules/native-module-tutorial/) next.
522