セットアップ
yarn add react-dom react-native-web
yarn add -D babel-plugin-react-native-web
-
public
ディレクトリを作り、中にファイルを追加-
index.html
を作る -
index.web.js
を作る -
webpack.config.js
を作る
-
-
package.json
にスクリプトを追加"web": "webpack serve --mode=development --config ./public/webpack.config.js”
public/webpack.config.jsのサンプル
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const appDirectory = path.resolve(__dirname, '../');
const babelConfig = require(`${appDirectory}/babel.config.js`);
const compileNodeModules = [
'@firebase',
'react-native-vector-icons',
'react-native-animatable',
'react-native-gesture-handler',
].map(moduleName => path.resolve(appDirectory, `node_modules/${moduleName}`));
module.exports = {
entry: path.join(__dirname, 'index.web.js'),
output: {
path: path.resolve(appDirectory, 'dist'),
publicPath: '/',
filename: 'rnw.bundle.js',
},
resolve: {
extensions: ['.web.tsx', '.web.ts', '.tsx', '.ts', '.web.js', '.js'],
alias: {
'react-native$': 'react-native-web',
'react-native-maps': 'react-native-web-maps',
},
},
module: {
rules: [
{
test: /\.(ts|tsx|js|jsx)$/,
include: [
path.resolve(__dirname, 'index.web.js'),
path.resolve(appDirectory, 'src'),
...compileNodeModules,
],
use: {
loader: 'babel-loader',
options: {
...babelConfig,
cacheDirectory: true,
},
},
},
{
test: /\.svg$/,
use: ['@svgr/webpack'],
},
{
test: /\.(gif|jpe?g|png)$/,
type: 'asset/resource',
parser: {
dataUrlCondition: {
maxSize: 8192, // 8KB
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'index.html'),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(appDirectory, 'public/Fonts/'),
to: path.resolve(appDirectory, 'dist/Fonts/'),
},
],
}),
],
};
public/index.web.jsのサンプル
import {AppRegistry} from 'react-native';
import {App} from '../src/App';
if (module.hot) {
module.hot.accept();
}
AppRegistry.registerComponent('React Native Web', () => App);
AppRegistry.runApplication('React Native Web', {
initialProps: {},
rootTag: document.getElementById('app-root'),
});
public/index.htmlのサンプル
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>RN Web</title>
<style>
#app-root {
display: flex;
flex: 1 1 100%;
height: 100vh;
}
</style>
<link rel="stylesheet" href="./font-style.css" />
</head>
<body>
<div id="app-root"></div>
</body>
</html>
webとそれ以外でコードを分ける方法
-
Platform.OS
を使って分ける方法import { Platform } from 'react-native';
-
Platform.select
を使って分ける方法Platform.select({web: ..., default: ...})
- ファイル名で分ける方法
-
*.web.js
のように.webがついていれば、webではそれが読み込まれる -
MyComponent.tsx
なら、MyComponent
フォルダを作り、MyComponent/index.tsx
MyComponent/index.web.tsx
とファイルを分ける -
index.android.tsx
index.ios.tsx
のようにAndroidとiOS専用にすることもできる
-
Alertが使えない
- alertコンポーネントを作り、それを使うように変更する
- ネイティブは
export const alert = Alert.alert
のようにそのままでOK - web用alert
-
window.confirm
を使う - Modalを使って実装する
-
react-native-firebaseが使えない
- 対応していないので、普通のfirebaseをインストールする
- Uncaught ReferenceError: require is not defined
- webpack.configの、js,jsx,ts,tsxのincludeに、@firebaseを追加
- webとネイティブで書き方が異なる
- 共通のインターフェースを用意してfirebaseの機能はそれを使うようにするか、webは名前空間方式(compat)を使うようにする
- phoneAuth
-
<View id='recaptcha' />
などを置いて、 invisible reCAPTCHAを使うnew RecaptchaVerifier(auth, 'recaptcha', {'size': 'invisible'})
-
PhoneAuthProvider.credential
にverificationId
と認証コードを渡してcredentialを作り、signInWithCredential
でサインインできる- もちろん
confirmationResult.confirm
でもよい
- もちろん
-
react-native-mapsが使えない
- react-native-web-mapsを入れ、aliasを設定し、index.htmlにスクリプトを追加
- Markerを使おうとすると、Element type is invalid
- import {Marker} ではなく、import MapViewからのMapView.Markerという古い形式で使わないと表示されない
react-native-vector-iconsが使えない
- webpackのコンパイルする対象にincludeする
- node_moduels/react-native-vector-icons/Fontsをpublicフォルダにコピー
- index.htmlのstyleなどで、
@font-face { src: url(Fonts/Ionicons.ttf); font-family: “Ionicons”; }
-
<link rel="stylesheet" href="./font-style.css" />
のように外出しすると見やすい
-
react-navigationがうまく表示されない
- 戻るボタンが表示されない
- Stack.Navigatorの
screenOptions
を設定するheaderBackTitleVisible: true,
-
...(Platform.OS === 'ios' && {headerBackImage: HeaderBackImage}),
- このHeaderBackImageは自作コンポーネント。vector-iconで「chevron-left」などを指定する
- Stack.Navigatorの
ローカルパッケージが使えない
- package.jsonで対象パッケージをnohoistにする
- 対象のパッケージを、webpackのコンパイルする対象にincludeする
レスポンシブ(メディアクエリ)に対応する
- デバイス幅を取得する
useWindowDimensions()
Dimensions.get('window').width
- ライブラリを使う
- react-responsiveを使う
- 自前のコンポーネントを用意する
responsive.tsxのサンプル
import {useWindowDimensions} from 'react-native';
const mobileWidth = 520;
/**
* モバイル端末の場合のみ表示する
* @example
* ```jsx
* <Mobile>モバイル端末のみ表示される</Mobile>
* ```
*/
export const Mobile = ({children}: {children: React.ReactNode}) => {
const {width} = useWindowDimensions();
const isMobile = width < mobileWidth;
return isMobile ? children : null;
};
/**
* タブレット端末の場合のみ表示する
* @example
* ```jsx
* <Tablet>タブレット端末のみ表示される</Tablet>
* ```
*/
export const Tablet = ({children}: {children: React.ReactNode}) => {
const {width} = useWindowDimensions();
const isMobile = width < mobileWidth;
return isMobile ? null : children;
};
type Media = 'mobile' | 'tablet';
/**
* 現在のメディアを取得する
* @returns {currentMedia: Media} 現在のメディア
*/
export const useCurrentMedia = (): {
currentMedia: Media;
} => {
const {width} = useWindowDimensions();
return {
currentMedia:
width < mobileWidth ? 'mobile' : 'tablet',
};
};