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```swift ios/ExpoWebViewModule.swift
183import ExpoModulesCore
184
185public class ExpoWebViewModule: Module {
186  public func definition() -> ModuleDefinition {
187    Name("ExpoWebView")
188
189    View(ExpoWebView.self) {
190      Prop("url") { (view, url: URL) in
191        if view.webView.url != url {
192          let urlRequest = URLRequest(url: url)
193          view.webView.load(urlRequest)
194        }
195      }
196    }
197  }
198}
199```
200
201### Android module
202
203```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
204package expo.modules.webview
205
206import expo.modules.kotlin.modules.Module
207import expo.modules.kotlin.modules.ModuleDefinition
208import java.net.URL
209
210class ExpoWebViewModule : Module() {
211  override fun definition() = ModuleDefinition {
212    Name("ExpoWebView")
213
214    View(ExpoWebView::class) {
215      Prop("url") { view: ExpoWebView, url: URL? ->
216        view.webView.loadUrl(url.toString())
217      }
218    }
219  }
220}
221```
222
223### TypeScript module
224
225All we need to do here is add the `url` prop to the `Props` type.
226
227```typescript src/ExpoWebView.tsx
228import { ViewProps } from 'react-native';
229import { requireNativeViewManager } from 'expo-modules-core';
230import * as React from 'react';
231
232export type Props = {
233  url?: string;
234} & ViewProps;
235
236const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
237
238export default function ExpoWebView(props: Props) {
239  return <NativeView {...props} />;
240}
241```
242
243### Example app
244
245Finally, we can pass in a URL to our WebView component in the example app.
246
247```typescript example/App.tsx
248import { WebView } from 'expo-web-view';
249
250export default function App() {
251  return <WebView style={{ flex: 1 }} url="https://expo.dev" />;
252}
253```
254
255When you rebuild and run the app, you will now see the Expo homepage.
256
257## 6. Add an event to notify when the page has loaded
258
259[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.
260
261### iOS view and module
262
263On iOS, we need to implement `webView(_:didFinish:)` and make ExpoWebView extend `WKNavigationDelegate`. We can then call the `onLoad` from that delegate method.
264
265```swift ios/ExpoWebView.swift
266import ExpoModulesCore
267import WebKit
268
269class ExpoWebView: ExpoView, WKNavigationDelegate {
270  let webView = WKWebView()
271  let onLoad = EventDispatcher()
272
273  required init(appContext: AppContext? = nil) {
274    super.init(appContext: appContext)
275    clipsToBounds = true
276    webView.navigationDelegate = self
277    addSubview(webView)
278  }
279
280  override func layoutSubviews() {
281    webView.frame = bounds
282  }
283
284  func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
285    if let url = webView.url {
286      onLoad([
287        "url": url.absoluteString
288      ])
289    }
290  }
291}
292```
293
294And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event.
295
296```swift ios/ExpoWebViewModule.swift
297import ExpoModulesCore
298
299public class ExpoWebViewModule: Module {
300  public func definition() -> ModuleDefinition {
301    Name("ExpoWebView")
302
303    View(ExpoWebView.self) {
304      Events("onLoad")
305
306      Prop("url") { (view, url: URL) in
307        if view.webView.url != url {
308          let urlRequest = URLRequest(url: url)
309          view.webView.load(urlRequest)
310        }
311      }
312    }
313  }
314}
315```
316
317### Android view and module
318
319On Android, we need to add override the `onPageFinished` function. We can then call the `onLoad` event handler that we defined in the module.
320
321```kotlin android/src/main/java/expo/modules/webview/ExpoWebView.kt
322package expo.modules.webview
323
324import android.content.Context
325import android.webkit.WebView
326import android.webkit.WebViewClient
327import expo.modules.kotlin.AppContext
328import expo.modules.kotlin.viewevent.EventDispatcher
329import expo.modules.kotlin.views.ExpoView
330
331class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
332  private val onLoad by EventDispatcher()
333
334  internal val webView = WebView(context).also {
335    it.layoutParams = LayoutParams(
336      LayoutParams.MATCH_PARENT,
337      LayoutParams.MATCH_PARENT
338    )
339
340    it.webViewClient = object : WebViewClient() {
341      override fun onPageFinished(view: WebView, url: String) {
342        onLoad(mapOf("url" to url))
343      }
344    }
345
346    addView(it)
347  }
348}
349```
350
351And we need to indicate in ExpoWebViewModule that the `View` has an `onLoad` event.
352
353```kotlin android/src/main/java/expo/modules/webview/ExpoWebViewModule.kt
354package expo.modules.webview
355
356import expo.modules.kotlin.modules.Module
357import expo.modules.kotlin.modules.ModuleDefinition
358import java.net.URL
359
360class ExpoWebViewModule : Module() {
361  override fun definition() = ModuleDefinition {
362    Name("ExpoWebView")
363
364    View(ExpoWebView::class) {
365      Events("onLoad")
366
367      Prop("url") { view: ExpoWebView, url: URL? ->
368        view.webView.loadUrl(url.toString())
369      }
370    }
371  }
372}
373```
374
375### TypeScript module
376
377Note 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`.
378
379```typescript src/ExpoWebView.tsx
380import { ViewProps } from 'react-native';
381import { requireNativeViewManager } from 'expo-modules-core';
382import * as React from 'react';
383
384export type OnLoadEvent = {
385  url: string;
386};
387
388export type Props = {
389  url?: string;
390  onLoad?: (event: { nativeEvent: OnLoadEvent }) => void;
391} & ViewProps;
392
393const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
394
395export default function ExpoWebView(props: Props) {
396  return <NativeView {...props} />;
397}
398```
399
400### Example app
401
402Now 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!
403
404```typescript example/App.tsx
405import { WebView } from 'expo-web-view';
406
407export default function App() {
408  return (
409    <WebView
410      style={{ flex: 1 }}
411      url="https://expo.dev"
412      onLoad={event => alert(`loaded ${event.nativeEvent.url}`)}
413    />
414  );
415}
416```
417
418## 7. Bonus: Build a web browser UI around it
419
420Now 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.
421
422<Collapsible summary="example/App.tsx">
423
424```typescript
425import { useState } from 'react';
426import { ActivityIndicator, Platform, Text, TextInput, View } from 'react-native';
427import { WebView } from 'expo-web-view';
428import { StatusBar } from 'expo-status-bar';
429
430export default function App() {
431  const [inputUrl, setInputUrl] = useState('https://docs.expo.dev/modules/');
432  const [url, setUrl] = useState(inputUrl);
433  const [isLoading, setIsLoading] = useState(true);
434
435  return (
436    <View style={{ flex: 1, paddingTop: Platform.OS === 'ios' ? 80 : 30 }}>
437      <TextInput
438        value={inputUrl}
439        onChangeText={setInputUrl}
440        returnKeyType="go"
441        autoCapitalize="none"
442        onSubmitEditing={() => {
443          if (inputUrl !== url) {
444            setUrl(inputUrl);
445            setIsLoading(true);
446          }
447        }}
448        keyboardType="url"
449        style={{
450          color: '#fff',
451          backgroundColor: '#000',
452          borderRadius: 10,
453          marginHorizontal: 10,
454          paddingHorizontal: 20,
455          height: 60,
456        }}
457      />
458
459      <WebView
460        url={url.startsWith('https://') || url.startsWith('http://') ? url : `https://${url}`}
461        onLoad={() => setIsLoading(false)}
462        style={{ flex: 1, marginTop: 20 }}
463      />
464      <LoadingView isLoading={isLoading} />
465      <StatusBar style="auto" />
466    </View>
467  );
468}
469
470function LoadingView({ isLoading }: { isLoading: boolean }) {
471  if (!isLoading) {
472    return null;
473  }
474
475  return (
476    <View
477      style={{
478        position: 'absolute',
479        bottom: 0,
480        left: 0,
481        right: 0,
482        height: 80,
483        backgroundColor: 'rgba(0,0,0,0.5)',
484        paddingBottom: 10,
485        justifyContent: 'center',
486        alignItems: 'center',
487        flexDirection: 'row',
488      }}>
489      <ActivityIndicator animating={isLoading} color="#fff" style={{ marginRight: 10 }} />
490      <Text style={{ color: '#fff' }}>Loading...</Text>
491    </View>
492  );
493}
494```
495
496</Collapsible>
497
498![A simple web browser UI built around our WebView](/static/images/modules/native-view-tutorial/web-browser.png)
499
500## Next steps
501
502Congratulations, 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/).
503
504if 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.
505