An easy way to integrate your React (or Preact/React Native Web) app into React Native app with WebView.
If you'd like to run your React web app in React Native, rewriting it for React Native or using react-native-web is preferred way in most cases. But sometimes rewriting is overkill, when you are just prototyping, or when the app includes something not available on React Native, like rich text editor with contenteditable or complicated logic with WebAssembly.
So how we run React app in React Native app as it is? It's logically possible if you run your web code in WebView using react-native-webview. However bundling React code with React Native is troublesome and implementing communication between React Native and WebView is so hard.
This library gives a bridge to make it easy. This will bundle the whole React app by some additional codes and it will be automatically re-compiled if you edit it. You rarely need to think which code you are editing for React or React Native, like isomorphic. The communication between React app and React Native app will be also simplified by this.
- Create React (or Preact/React Native Web) app bundle for WebView automatically in build process of React Native
- Handle communication between React Native and WebView with React hook style
- With
useWebViewMessage
hook, you can subscribe messages from WebView. - With
useNativeMessage
hook, you can subscribe messages from React Native. emit
function sends message.
- With
- Support bundling some assets in web side with ES6 import syntax
.json
is imported as an object, like require in Node.js..txt
and.md
are imported as string, like raw-loader..css
is injected to the HTML head of WebView, like css-loader with style-loader..bmp
,.gif
,.png
,.jpg
,.jpeg
,.webp
and.svg
are loaded as base64 encoded url, like url-loader..htm
and.html
are loaded as string, which can be rendered with React's dangerouslySetInnerHTML..wasm
is imported like Node.js, which is compatible with ES Module Integration Proposal for WebAssembly.
If you have some feature requests or improvements, please create a issue or PR.
npm install react-native-react-bridge react-native-webview
# Necessary only if you render React app in WebView
npm install react-dom
# Necessary only if you render Preact app in WebView
# preact >= 10.0
npm install preact
# Necessary only if you render React Native Web app in WebView
npm install react-dom react-native-web
- react >= 16.14
- react-native >= 0.60
module.exports = {
transformer: {
// This detects entry points of React app and transforms them
// For the other files this will switch to use default `metro-react-native-babel-transformer` for transforming
babelTransformerPath: require.resolve('react-native-react-bridge/lib/plugin'),
...
},
/*
// optional config
rnrb: {
// Set `true` if you use Preact in web side.
// This will alias imports from `react` and `react-dom` to `preact/compat` automatically.
preact: true,
// Set `true` if you use react-native-web in web side.
// This will alias imports from `react-native` to `react-native-web` automatically.
web: true
},
*/
...
};
const { getDefaultConfig } = require("expo/metro-config");
const config = getDefaultConfig(__dirname);
config.transformer.babelTransformerPath = require.resolve(
"react-native-react-bridge/lib/plugin"
);
module.exports = config;
If your project at some point requires a metro configuration with additional transformers, consider making a separate customTransformer.js
file in the project root with logic for delegating files types to the appropriate transformer, and modifying metro.config.js
file to reference the customer transformer file. For example, if you are using react-native-svg-transformer
, this would be your custom transformer file:
// root/customTransformer.js
const reactNativeReactBridgeTransformer = require("react-native-react-bridge/lib/plugin");
const svgTransformer = require("react-native-svg-transformer");
module.exports.transform = function ({ src, filename, options }) {
if (filename.endsWith(".svg")) {
return svgTransformer.transform({ src, filename, options });
} else {
return reactNativeReactBridgeTransformer.transform({
src,
filename,
options,
});
}
};
And this would be your metro config:
// root/metro.config.js
const { getDefaultConfig } = require("metro-config");
module.exports = (async () => {
const {
resolver: { sourceExts, assetExts },
} = await getDefaultConfig();
return {
transformer: {
babelTransformerPath: require.resolve("./customTransformer.js"),
},
resolver: {
assetExts: assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...sourceExts, "svg"],
},
};
})();
To support custom Esbuild options, we can use Multiple Transformers method and replace the customTransformer.js file with the following code:
// root/customTransformer.js
const reactNativeReactBridgeTransformer = require("react-native-react-bridge/lib/plugin");
const esbuildOptions = {
pluglins: [],
};
const transform =
reactNativeReactBridgeTransformer.createTransformer(esbuildOptions);
module.exports.transform = function ({ src, filename, options }) {
return transform({ src, filename, options });
};
- If you use React, React Native Web or Preact with React alias, import modules
react-native-react-bridge/lib/web
. - If you use Preact, import modules from
react-native-react-bridge/lib/web/preact
.
// WebApp.js
import React, { useState } from "react";
import {
webViewRender,
emit,
useNativeMessage,
} from "react-native-react-bridge/lib/web";
// Importing css is supported
import "./example.css";
// Images are loaded as base64 encoded string
import image from "./foo.png";
const Root = () => {
const [data, setData] = useState("");
// useNativeMessage hook receives message from React Native
useNativeMessage((message) => {
if (message.type === "success") {
setData(message.data);
}
});
return (
<div>
<img src={image} />
<div>{data}</div>
<button
onClick={() => {
// emit sends message to React Native
// type: event name
// data: some data which will be serialized by JSON.stringify
emit({ type: "hello", data: 123 });
}}
/>
</div>
);
};
// This statement is detected by babelTransformer as an entry point
// All dependencies are resolved, compressed and stringified into one file
export default webViewRender(<Root />);
// App.js
import React from "react";
import WebView from "react-native-webview";
import { useWebViewMessage } from "react-native-react-bridge";
import webApp from "./WebApp";
const App = () => {
// useWebViewMessage hook create props for WebView and handle communication
// The argument is callback to receive message from React
const { ref, onMessage, emit } = useWebViewMessage((message) => {
// emit sends message to React
// type: event name
// data: some data which will be serialized by JSON.stringify
if (message.type === "hello" && message.data === 123) {
emit({ type: "success", data: "succeeded!" });
}
});
return (
<WebView
// ref, source and onMessage must be passed to react-native-webview
ref={ref}
// Pass the source code of React app
source={{ html: webApp }}
onMessage={onMessage}
/>
);
};
react-native-webview has some ways to show errors occurred in webview. This may be helpful to troubleshoot it.
https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#onerror
This repository includes demo app.
git clone [email protected]:inokawa/react-native-react-bridge.git
cd examples/DemoApp
npm install
npm run ios # or npm run android
All contributions are welcome. If you find a problem, feel free to create an issue or a PR.
- Fork this repo.
- Run
npm install
. - Commit your fix.
- Add tests to cover the fix.
- Make a PR and confirm all the CI checks passed.