Edit this page
A tutorial on creating a native view that renders a WebView with Expo Modules API.
In this tutorial, you'll build an example module with a native view that renders a WebView. For Android, you'll use the WebView
component, and for iOS, WKWebView
. Web support can be implemented using an iframe
and is left as an exercise for you.
1
2
Clean up the default module to start with a clean slate by deleting the following files:
-Â
cd expo-web-view
-Â
rm src/ExpoWebView.types.ts src/ExpoWebViewModule.ts
-Â
rm src/ExpoWebView.web.tsx src/ExpoWebViewModule.web.ts
Locate the following files and replace them with the provided minimal boilerplate:
package expo.modules.webview
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoWebViewModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoWebView")
View(ExpoWebView::class) {}
}
}
import ExpoModulesCore
public class ExpoWebViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoWebView")
View(ExpoWebView.self) {}
}
}
import { ViewProps } from 'react-native';
import { requireNativeViewManager } from 'expo-modules-core';
import * as React from 'react';
export type Props = ViewProps;
const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
export default function ExpoWebView(props: Props) {
return <NativeView {...props} />;
}
export { default as WebView, Props as WebViewProps } from './ExpoWebView';
import { WebView } from 'expo-web-view';
export default function App() {
return <WebView style={{ flex: 1, backgroundColor: 'purple' }} />;
}
3
To ensure everything is working, start the TypeScript compiler to watch for changes and rebuild the module's JavaScript:
# Run this in the root of the project to start the TypeScript compiler
-Â
npm run build
# Navigate to the example directory
-Â
cd example
# Run the example app on Android
-Â
npx expo run:android
# Run the example app on iOS
-Â
npx expo run:ios
You should now see a blank purple screen. While it's not very exciting, it's a good start. Next, turn it into a WebView.
4
Add the system WebView
with a hardcoded URL as a subview of ExpoWebView
. The ExpoWebView
class extends ExpoView
, which extends RCTView
from React Native, and eventually extends View
on Android and UIView
on iOS.
Ensure that the WebView
subview uses the same layout as ExpoWebView
, whose layout is calculated by React Native's layout engine.
On Android, use LayoutParams
to set the WebView's layout to match the ExpoWebView
layout. You can do this when you instantiate the WebView.
package expo.modules.webview
import android.content.Context
import android.webkit.WebView
import android.webkit.WebViewClient
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.views.ExpoView
class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
internal val webView = WebView(context).also {
it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
it.webViewClient = object : WebViewClient() {}
addView(it)
it.loadUrl("https://docs.expo.dev/modules/")
}
}
On iOS, set clipsToBounds
to true
and ensure the WebView's frame
matches the bounds of ExpoWebView
in layoutSubviews
. The init
method is called when the view is created, and layoutSubviews
is called when the layout changes.
import ExpoModulesCore
import WebKit
class ExpoWebView: ExpoView {
let webView = WKWebView()
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
clipsToBounds = true
addSubview(webView)
let url = URL(string:"https://docs.expo.dev/modules/")!
let urlRequest = URLRequest(url:url)
webView.load(urlRequest)
}
override func layoutSubviews() {
webView.frame = bounds
}
}
No changes are required. Rebuild and run the app using the following commands:
# Prebuild the example app with the --clean flag to ensure a clean build
-Â
npx expo prebuild --clean
# Run the example app on Android
-Â
npx expo run:android
# Run the example app on iOS
-Â
npx expo run:ios
After that, you'll see the Expo Modules API overview page rendered. If the changes aren't reflected, try reinstalling the app.
5
To set a prop on the view, define the prop name and setter inside ExpoWebViewModule
. In this case, you can access the webView
property directly for convenience. However, in real-world scenarios, keep the logic inside the ExpoWebView
class to minimize how much ExpoWebViewModule
knows about its internals.
Use the Prop definition component to define the prop. In the prop setter block, you can access both the view and the prop. Specify that the URL is of type URL
— the Expo modules API will convert strings to the native URL
type.
package expo.modules.webview
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.net.URL
class ExpoWebViewModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoWebView")
View(ExpoWebView::class) {
Prop("url") { view: ExpoWebView, url: URL? ->
view.webView.loadUrl(url.toString())
}
}
}
}
import ExpoModulesCore
public class ExpoWebViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoWebView")
View(ExpoWebView.self) {
Prop("url") { (view, url: URL) in
if view.webView.url != url {
let urlRequest = URLRequest(url: url)
view.webView.load(urlRequest)
}
}
}
}
}
Next, add the url
prop to the Props
type.
import { ViewProps } from 'react-native';
import { requireNativeViewManager } from 'expo-modules-core';
import * as React from 'react';
export type Props = {
url?: string;
} & ViewProps;
const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
export default function ExpoWebView(props: Props) {
return <NativeView {...props} />;
}
Finally, pass a URL
to your WebView
component in the example app.
import { WebView } from 'expo-web-view';
export default function App() {
return <WebView style={{ flex: 1 }} url="https://expo.dev" />;
}
Rebuild the example app:
-Â
npx expo prebuild --clean
# Run the example app on Android
-Â
npx expo run:android
# Run the example app on iOS
-Â
npx expo run:ios
After that, you'll see the Expo homepage in the WebView.
6
View callbacks allow developers to listen for events on components. They are typically registered through props on the component, for example: <Image onLoad={...} />
. Use the Events definition component to define an event for your WebView. Call it onLoad
.
On Android, override the onPageFinished
function. Then, call the onLoad
event handler that you defined in the module.
package expo.modules.webview
import android.content.Context
import android.webkit.WebView
import android.webkit.WebViewClient
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView
class ExpoWebView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
private val onLoad by EventDispatcher()
internal val webView = WebView(context).also {
it.layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
it.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
onLoad(mapOf("url" to url))
}
}
addView(it)
}
}
Indicate in ExpoWebViewModule
that the View
has an onLoad
event.
package expo.modules.webview
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.net.URL
class ExpoWebViewModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoWebView")
View(ExpoWebView::class) {
Events("onLoad")
Prop("url") { view: ExpoWebView, url: URL? ->
view.webView.loadUrl(url.toString())
}
}
}
}
On iOS, implement webView(_:didFinish:)
and make ExpoWebView
extend WKNavigationDelegate
. Then, call onLoad
from that delegate method.
import ExpoModulesCore
import WebKit
class ExpoWebView: ExpoView, WKNavigationDelegate {
let webView = WKWebView()
let onLoad = EventDispatcher()
required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
clipsToBounds = true
webView.navigationDelegate = self
addSubview(webView)
}
override func layoutSubviews() {
webView.frame = bounds
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if let url = webView.url {
onLoad([
"url": url.absoluteString
])
}
}
}
Indicate in ExpoWebViewModule
that the View
has an onLoad
event.
import ExpoModulesCore
public class ExpoWebViewModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoWebView")
View(ExpoWebView.self) {
Events("onLoad")
Prop("url") { (view, url: URL) in
if view.webView.url != url {
let urlRequest = URLRequest(url: url)
view.webView.load(urlRequest)
}
}
}
}
}
Event payloads are included within the nativeEvent
property of the event. To access the url
from the onLoad
event, read event.nativeEvent.url
.
import { ViewProps } from 'react-native';
import { requireNativeViewManager } from 'expo-modules-core';
import * as React from 'react';
export type OnLoadEvent = {
url: string;
};
export type Props = {
url?: string;
onLoad?: (event: { nativeEvent: OnLoadEvent }) => void;
} & ViewProps;
const NativeView: React.ComponentType<Props> = requireNativeViewManager('ExpoWebView');
export default function ExpoWebView(props: Props) {
return <NativeView {...props} />;
}
Update the example app to show an alert when the page has loaded. Copy the following code, then rebuild and run your app, and you'll see the alert!
import { WebView } from 'expo-web-view';
export default function App() {
return (
<WebView
style={{ flex: 1 }}
url="https://expo.dev"
onLoad={event => alert(`loaded ${event.nativeEvent.url}`)}
/>
);
}
7
Now that you have a WebView, build a web browser UI around it. Try rebuilding a browser UI, and feel free to add new native capabilities as needed (for example, support for back or reload buttons). If you need inspiration, see the example below.
import { useState } from 'react';
import { ActivityIndicator, Platform, Text, TextInput, View } from 'react-native';
import { WebView } from 'expo-web-view';
export default function App() {
const [inputUrl, setInputUrl] = useState('https://docs.expo.dev/modules/');
const [url, setUrl] = useState(inputUrl);
const [isLoading, setIsLoading] = useState(true);
return (
<View style={{ flex: 1, paddingTop: Platform.OS === 'ios' ? 80 : 30 }}>
<TextInput
value={inputUrl}
onChangeText={setInputUrl}
returnKeyType="go"
autoCapitalize="none"
onSubmitEditing={() => {
if (inputUrl !== url) {
setUrl(inputUrl);
setIsLoading(true);
}
}}
keyboardType="url"
style={{
color: '#fff',
backgroundColor: '#000',
borderRadius: 10,
marginHorizontal: 10,
paddingHorizontal: 20,
height: 60,
}}
/>
<WebView
url={url.startsWith('https://') || url.startsWith('http://') ? url : `https://${url}`}
onLoad={() => setIsLoading(false)}
style={{ flex: 1, marginTop: 20 }}
/>
<LoadingView isLoading={isLoading} />
</View>
);
}
function LoadingView({ isLoading }: { isLoading: boolean }) {
if (!isLoading) {
return null;
}
return (
<View
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 80,
backgroundColor: 'rgba(0,0,0,0.5)',
paddingBottom: 10,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
}}>
<ActivityIndicator animating={isLoading} color="#fff" style={{ marginRight: 10 }} />
<Text style={{ color: '#fff' }}>Loading...</Text>
</View>
);
}
Congratulations! You've created your first Expo module with a native view for Android and iOS.
Create native modules using Kotlin and Swift.
A tutorial on creating a native module that persists settings with Expo Modules API.