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 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