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 { CodeBlocksTable } from '~/components/plugins/CodeBlocksTable';
8import { PlatformTag } from '~/ui/components/Tag';
9import { APIBox } from '~/components/plugins/APIBox';
10import { Terminal } from '~/ui/components/Snippet';
11import { Collapsible } from '~/ui/components/Collapsible';
12
13In 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.
14
15## 1. Initialize a new module
16
17First, 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:
18
19<Terminal cmd={['$ npx create-expo-module expo-web-view']} />
20
21> **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.
22
23## 2. Set up our workspace
24
25Now 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.
26
27<Terminal
28  cmdCopy="cd expo-web-view && rm src/ExpoWebView.types.ts src/ExpoWebView.web.tsx src/ExpoWebViewModule.ts src/ExpoWebViewModule.web.ts"
29  cmd={[
30    '$ cd expo-web-view',
31    '$ rm src/ExpoWebView.types.ts src/ExpoWebViewModule.ts',
32    '$ rm src/ExpoWebView.web.tsx src/ExpoWebViewModule.web.ts',
33  ]}
34/>
35
36Find the following files and replace them with the provided minimal boilerplate:
37
38```swift ios/ExpoWebViewModule.swift
39import ExpoModulesCore
40
41public class ExpoWebViewModule: Module {
42  public func definition() -> ModuleDefinition {
43    Name("ExpoWebView")
44
45    View(ExpoWebView.self) {}
46  }
47}
48```
49
50```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
51package expo.modules.webview
52
53import expo.modules.kotlin.modules.Module
54import expo.modules.kotlin.modules.ModuleDefinition
55
56class ExpoWebViewModule : Module() {
57  override fun definition() = ModuleDefinition {
58    Name("ExpoWebView")
59
60    View(ExpoWebView::class) {}
61  }
62}
63```
64
65```typescript src/index.ts
66export { default as WebView, Props as WebViewProps } from './ExpoWebView';
67```
68
69```typescript src/ExpoWebView.tsx
70import { ViewProps } from 'react-native';
71import { requireNativeViewManager } from 'expo-modules-core';
72import * as React from 'react';
73
74export type Props = ViewProps;
75
76const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
77
78export default function ExpoWebView(props: Props) {
79  return <NativeView {...props} />;
80}
81```
82
83```typescript example/App.tsx
84import { WebView } from 'expo-web-view';
85
86export default function App() {
87  return <WebView style={{ flex: 1, backgroundColor: 'purple' }} />;
88}
89```
90
91## 3. Run the example project
92
93Now 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.
94
95<Terminal
96  cmdCopy="npm run build"
97  cmd={[
98    '# Run this in the root of the project to start the TypeScript compiler',
99    '$ npm run build',
100  ]}
101/>
102
103<Terminal
104  cmdCopy="cd example && npx expo run:ios"
105  cmd={[
106    '$ cd example',
107    '# Run the example app on iOS',
108    '$ npx expo run:ios',
109    '# Run the example app on Android',
110    '$ npx expo run:android',
111  ]}
112/>
113
114We 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.
115
116## 4. Add the system WebView as a subview
117
118Now 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.
119
120### iOS view
121
122On 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.
123
124```swift ios/ExpoWebView.swift
125import ExpoModulesCore
126import WebKit
127
128class ExpoWebView: ExpoView {
129  let webView = WKWebView()
130
131  required init(appContext: AppContext? = nil) {
132    super.init(appContext: appContext)
133    clipsToBounds = true
134    addSubview(webView)
135
136    let url =  URL(string:"https://docs.expo.dev/modules/")!
137    let urlRequest = URLRequest(url:url)
138    webView.load(urlRequest)
139  }
140
141  override func layoutSubviews() {
142    webView.frame = bounds
143  }
144}
145```
146
147### Android view
148
149On 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.
150
151```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt
152package expo.modules.webview
153
154import android.content.Context
155import android.webkit.WebView
156import android.webkit.WebViewClient
157import expo.modules.kotlin.AppContext
158import expo.modules.kotlin.views.ExpoView
159
160class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
161  internal val webView = WebView(context).also {
162    it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
163    it.webViewClient = object : WebViewClient() {}
164    addView(it)
165
166    it.loadUrl("https://docs.expo.dev/modules/")
167  }
168}
169```
170
171### Example app
172
173No changes are needed, we can rebuild and run the app and you will see the [Expo Modules API overview page](/modules/).
174
175## 5. Add a prop to set the URL
176
177To 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`.
178
179We 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.
180
181### iOS module
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 { ActivityIndicator, Platform, Text, TextInput, View } from 'react-native';
428import { WebView } from 'expo-web-view';
429import { StatusBar } from 'expo-status-bar';
430
431export default function App() {
432  const [inputUrl, setInputUrl] = useState('https://docs.expo.dev/modules/');
433  const [url, setUrl] = useState(inputUrl);
434  const [isLoading, setIsLoading] = useState(true);
435
436  return (
437    <View style={{ flex: 1, paddingTop: Platform.OS === 'ios' ? 80 : 30 }}>
438      <TextInput
439        value={inputUrl}
440        onChangeText={setInputUrl}
441        returnKeyType="go"
442        autoCapitalize="none"
443        onSubmitEditing={() => {
444          if (inputUrl !== url) {
445            setUrl(inputUrl);
446            setIsLoading(true);
447          }
448        }}
449        keyboardType="url"
450        style={{
451          color: '#fff',
452          backgroundColor: '#000',
453          borderRadius: 10,
454          marginHorizontal: 10,
455          paddingHorizontal: 20,
456          height: 60,
457        }}
458      />
459
460      <WebView
461        url={url.startsWith('https://') || url.startsWith('http://') ? url : `https://${url}`}
462        onLoad={() => setIsLoading(false)}
463        style={{ flex: 1, marginTop: 20 }}
464      />
465      <LoadingView isLoading={isLoading} />
466      <StatusBar style="auto" />
467    </View>
468  );
469}
470
471function LoadingView({ isLoading }: { isLoading: boolean }) {
472  if (!isLoading) {
473    return null;
474  }
475
476  return (
477    <View
478      style={{
479        position: 'absolute',
480        bottom: 0,
481        left: 0,
482        right: 0,
483        height: 80,
484        backgroundColor: 'rgba(0,0,0,0.5)',
485        paddingBottom: 10,
486        justifyContent: 'center',
487        alignItems: 'center',
488        flexDirection: 'row',
489      }}>
490      <ActivityIndicator animating={isLoading} color="#fff" style={{ marginRight: 10 }} />
491      <Text style={{ color: '#fff' }}>Loading...</Text>
492    </View>
493  );
494}
495```
496
497</Collapsible>
498
499![A simple web browser UI built around our WebView](/static/images/modules/native-view-tutorial/web-browser.png)
500
501## Next steps
502
503Congratulations, 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/).
504
505if 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.
506