Expo + React Native のプロジェクトで Next.js のようにファイルベースのルーティングができるようになりました。
Expo Router を使ってみます。
Expo Router をインストール
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar react-native-gesture-handler
Entry point を設定する
package.json
の エントリーポイントを変更します。
{
"main": "expo-router/entry"
}
app.json に scheme を設定する
app.json
にディープリンク用の schme を設定します。
{
"scheme": "your-app-scheme"
}
Web で利用する場合は、 bundler
の設定も追加します。
{
"web": {
"bundler": "metro"
}
}
babel.config.js に plugin を追加
plugins
に 'expo-router/babel'
を追加します。
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['expo-router/babel'],
};
};
app ディレクトリに index.tsx を作成
ルートディレクトリに app
ディレクトリを作ります。
app/index.tsx
となるように、 index.tsx
を作ります。
import { Text } from 'react-native';
export default function Page() {
return <Text>Home Page</Text>;
}
これでアプリを起動すると、 Home Page と表示されます。
App.tsx の内容は表示されなくなっている点に注目です。
<Link> でリンクを張ってみる
以下のようなディレクトリ構成を作ります。
$ tree app
app
├── index.tsx
└── samples
└── skia-circular-progress.tsx
app/index.tsx
は以下のとおりです。
Link href でリンクをはって移動させることができます。
import { View, Text, StyleSheet } from 'react-native';
import { Link } from 'expo-router';
export default function Page() {
return (
<View style={styles.container}>
<Link href={'/samples/skia-circular-progress'} style={styles.linkButton}>
<Text style={styles.linkText}>Skia Circular Progress</Text>
</Link>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
linkButton: {
backgroundColor: '#304FFE',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
borderColor: '#0056b3',
borderWidth: 1,
},
linkText: {
color: 'white',
fontSize: 14,
textAlign: 'center',
},
});
samples/skia-circular-progress.tsx
は以下のようにしています。
import { Link } from 'expo-router';
import { StyleSheet, View, Text, PixelRatio, Pressable } from 'react-native';
import { Easing, runTiming, useFont, useValue } from '@shopify/react-native-skia';
import CircularProgress from '../../components/CircularProgress';
const RADIUS = PixelRatio.roundToNearestPixel(130);
const STROKE_WIDTH = 12;
export default function Page() {
const percentageComplete = 0.85;
const animationState = useValue(0);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const font = useFont(require('../../Roboto-Light.ttf'), 60);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const smallerFont = useFont(require('../../Roboto-Light.ttf'), 25);
const animateChart = () => {
animationState.current = 0;
runTiming(animationState, percentageComplete, {
duration: 1250,
easing: Easing.inOut(Easing.cubic),
});
};
const isFontLoaded = font != null && smallerFont != null;
return (
<View style={styles.container}>
<View style={styles.linkContainer}>
<Link href={'/'} style={styles.linkButton}>
<Text style={styles.linkText}>Home</Text>
</Link>
</View>
{isFontLoaded && (
<>
<View style={styles.donutChartContainer}>
<CircularProgress
strokeWidth={STROKE_WIDTH}
radius={RADIUS}
percentageCompleted={animationState}
font={font}
smallerFont={smallerFont}
targetPercentage={percentageComplete}
/>
</View>
<Pressable onPress={animateChart} style={styles.button}>
<Text style={styles.buttonText}>Animate !</Text>
</Pressable>
</>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'flex-start',
},
linkContainer: {
marginVertical: 16,
},
linkButton: {
backgroundColor: '#FF5722',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
borderColor: '#F4511E',
borderWidth: 1,
},
linkText: {
color: 'white',
fontSize: 14,
textAlign: 'center',
},
donutChartContainer: {
height: RADIUS * 2,
width: RADIUS * 2,
},
button: {
marginTop: 40,
backgroundColor: 'blue',
paddingHorizontal: 60,
paddingVertical: 15,
borderRadius: 10,
},
buttonText: {
color: 'white',
fontSize: 20,
},
});
ファイル名でリンクが設定されていることがわかります。
突然謎のエラーが出た
iOS Bundling failed 1015ms
error: node_modules/expo-router/_ctx.ios.tsx: node_modules/expo-router/_ctx.ios.tsx:Invalid call at line 2: process.env.EXPO_ROUTER_APP_ROOT
First argument of `require.context` should be a string denoting the directory to require.
2023年12月に新たなプロジェクトを作ろうとしたら、同じ手順でエラーが出るようになりました。
原因は謎です。
一番簡単な回避策は初期テンプレートをそのまま流用する形でプロジェクトを作ることです。
npx create-expo-app@latest --template tabs@49
公式ドキュメントには babel.config.js に plugins: ['expo-router/babel']
の記述はいらない、と書いてありますが、テンプレのコードには plugins: ['expo-router/babel']
があるので、もしかしたら babel のプラグインの追加が必要なのかもしれません(未検証)