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