Expo bareExpo RouterReact NativeReact Native Paper

React Navigation環境からExpo Routerに乗り換える方法〜React Native Paper〜

expo-router-eye-catch Expo bare
スポンサーリンク

マテリアル デザインライブラリ・コンポーネントライブラリである React Native Paper を Expo Router と連携するための手順をご紹介します。React Native Paper + React Navigation環境から乗り換える方法にもなると思います。

Expo Router環境を作成する

次の記事通り開発環境は整ったとします。

React Native Paperを設定する

ライブラリを設置・連携する

ライブラリを設置します。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
try🐶everything casablanca$ yarn add react-native-paper
try🐶everything casablanca$ yarn add react-native-safe-area-context
try🐶everything casablanca$ npx pod-install
try🐶everything casablanca$ yarn add react-native-paper try🐶everything casablanca$ yarn add react-native-safe-area-context try🐶everything casablanca$ npx pod-install
try🐶everything casablanca$ yarn add react-native-paper
try🐶everything casablanca$ yarn add react-native-safe-area-context
try🐶everything casablanca$ npx pod-install

使用しないモジュールを除外してバンドル サイズを小さくする設定をします。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['expo-router/babel'],
env: {production: {plugins: ['react-native-paper/babel']}}, // <-- 追加
};
};
// babel.config.js module.exports = function (api) { api.cache(true); return { presets: ['babel-preset-expo'], plugins: ['expo-router/babel'], env: {production: {plugins: ['react-native-paper/babel']}}, // <-- 追加 }; };
// babel.config.js

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: ['expo-router/babel'],
    env: {production: {plugins: ['react-native-paper/babel']}}, // <-- 追加
  };
};

これで基本設定は完了です!

次は、Expo Routerと連携します。(app/_layout.tsx)

変化を確認するため、theme.colors.{primary|secondary}の値をMD2Colors.xxxに変更します。

  • Line 3-7,15,16

ルートコンポーネントを、<PaperProvider /> でラップします。

  • Line 59, 64
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/_layout.tsx
import React, {useEffect} from 'react';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import {
MD2Colors,
PaperProvider,
MD3LightTheme as DefaultTheme,
} from 'react-native-paper';
import {useFonts} from 'expo-font';
import {SplashScreen, Stack} from 'expo-router';
const theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: MD2Colors.greenA700,
secondary: MD2Colors.amberA400,
},
};
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router';
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(tabs)',
};
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
SpaceMono: require('common/assets/fonts/SpaceMono-Regular.ttf'),
...FontAwesome.font,
});
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
if (error) throw error;
}, [error]);
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
if (!loaded) {
return null;
}
return <RootLayoutNav />;
}
function RootLayoutNav() {
return (
<PaperProvider theme={theme}>
<Stack>
<Stack.Screen name="(tabs)" options={{headerShown: false}} />
<Stack.Screen name="modal" options={{presentation: 'modal'}} />
</Stack>
</PaperProvider>
);
}
// app/_layout.tsx import React, {useEffect} from 'react'; import FontAwesome from '@expo/vector-icons/FontAwesome'; import { MD2Colors, PaperProvider, MD3LightTheme as DefaultTheme, } from 'react-native-paper'; import {useFonts} from 'expo-font'; import {SplashScreen, Stack} from 'expo-router'; const theme = { ...DefaultTheme, colors: { ...DefaultTheme.colors, primary: MD2Colors.greenA700, secondary: MD2Colors.amberA400, }, }; export { // Catch any errors thrown by the Layout component. ErrorBoundary, } from 'expo-router'; export const unstable_settings = { // Ensure that reloading on `/modal` keeps a back button present. initialRouteName: '(tabs)', }; // Prevent the splash screen from auto-hiding before asset loading is complete. SplashScreen.preventAutoHideAsync(); export default function RootLayout() { const [loaded, error] = useFonts({ SpaceMono: require('common/assets/fonts/SpaceMono-Regular.ttf'), ...FontAwesome.font, }); // Expo Router uses Error Boundaries to catch errors in the navigation tree. useEffect(() => { if (error) throw error; }, [error]); useEffect(() => { if (loaded) { SplashScreen.hideAsync(); } }, [loaded]); if (!loaded) { return null; } return <RootLayoutNav />; } function RootLayoutNav() { return ( <PaperProvider theme={theme}> <Stack> <Stack.Screen name="(tabs)" options={{headerShown: false}} /> <Stack.Screen name="modal" options={{presentation: 'modal'}} /> </Stack> </PaperProvider> ); }
// app/_layout.tsx

import React, {useEffect} from 'react';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import {
  MD2Colors,
  PaperProvider,
  MD3LightTheme as DefaultTheme,
} from 'react-native-paper';
import {useFonts} from 'expo-font';
import {SplashScreen, Stack} from 'expo-router';

const theme = {
  ...DefaultTheme,
  colors: {
    ...DefaultTheme.colors,
    primary: MD2Colors.greenA700,
    secondary: MD2Colors.amberA400,
  },
};

export {
  // Catch any errors thrown by the Layout component.
  ErrorBoundary,
} from 'expo-router';

export const unstable_settings = {
  // Ensure that reloading on `/modal` keeps a back button present.
  initialRouteName: '(tabs)',
};

// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const [loaded, error] = useFonts({
    SpaceMono: require('common/assets/fonts/SpaceMono-Regular.ttf'), 
    ...FontAwesome.font,
  });

  // Expo Router uses Error Boundaries to catch errors in the navigation tree.
  useEffect(() => {
    if (error) throw error;
  }, [error]);

  useEffect(() => {
    if (loaded) {
      SplashScreen.hideAsync();
    }
  }, [loaded]);

  if (!loaded) {
    return null;
  }

  return <RootLayoutNav />;
}

function RootLayoutNav() {
  return (
    <PaperProvider theme={theme}>
      <Stack>
        <Stack.Screen name="(tabs)" options={{headerShown: false}} />
        <Stack.Screen name="modal" options={{presentation: 'modal'}} />
      </Stack>
    </PaperProvider>
  );
}

カスタマイズする

ヘッダーを <AppBar/> に変更します (app/(tabs)/_layout.tsx)

Appbar.Header の backgroundColor を colors.primary に設定します。

  • Line 5,7,19-20,32-39

navigation.canGoBack() を使って、ReactNavigation の back Prop を代替できます。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/(tabs)/_layout.tsx
import * as React from 'react';
import {Pressable, useColorScheme} from 'react-native';
import {Appbar, useTheme} from 'react-native-paper';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import {Link, Tabs, useNavigation} from 'expo-router';
import Colors from 'common/constants/colors';
function TabBarIcon(props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) {
return <FontAwesome size={28} style={{marginBottom: -3}} {...props} />;
}
export default function TabLayout() {
const colorScheme = useColorScheme();
const {colors} = useTheme();
const navigation = useNavigation();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
}}>
<Tabs.Screen
name="index"
options={{
title: 'Tab One',
tabBarIcon: ({color}) => <TabBarIcon name="code" color={color} />,
header: () => (
<Appbar.Header style={{backgroundColor: colors.primary}}>
{navigation.canGoBack() ? (
<Appbar.BackAction onPress={navigation.goBack} />
) : null}
<Appbar.Content title={'Tab One'} />
</Appbar.Header>
),
headerRight: () => (
<Link href="/modal" asChild>
<Pressable>
{({pressed}) => (
<FontAwesome
name="info-circle"
size={25}
color={Colors[colorScheme ?? 'light'].text}
style={{marginRight: 15, opacity: pressed ? 0.5 : 1}}
/>
)}
</Pressable>
</Link>
),
}}
/>
<Tabs.Screen
name="two"
...
// app/(tabs)/_layout.tsx import * as React from 'react'; import {Pressable, useColorScheme} from 'react-native'; import {Appbar, useTheme} from 'react-native-paper'; import FontAwesome from '@expo/vector-icons/FontAwesome'; import {Link, Tabs, useNavigation} from 'expo-router'; import Colors from 'common/constants/colors'; function TabBarIcon(props: { name: React.ComponentProps<typeof FontAwesome>['name']; color: string; }) { return <FontAwesome size={28} style={{marginBottom: -3}} {...props} />; } export default function TabLayout() { const colorScheme = useColorScheme(); const {colors} = useTheme(); const navigation = useNavigation(); return ( <Tabs screenOptions={{ tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, }}> <Tabs.Screen name="index" options={{ title: 'Tab One', tabBarIcon: ({color}) => <TabBarIcon name="code" color={color} />, header: () => ( <Appbar.Header style={{backgroundColor: colors.primary}}> {navigation.canGoBack() ? ( <Appbar.BackAction onPress={navigation.goBack} /> ) : null} <Appbar.Content title={'Tab One'} /> </Appbar.Header> ), headerRight: () => ( <Link href="/modal" asChild> <Pressable> {({pressed}) => ( <FontAwesome name="info-circle" size={25} color={Colors[colorScheme ?? 'light'].text} style={{marginRight: 15, opacity: pressed ? 0.5 : 1}} /> )} </Pressable> </Link> ), }} /> <Tabs.Screen name="two" ...
// app/(tabs)/_layout.tsx

import * as React from 'react';
import {Pressable, useColorScheme} from 'react-native';
import {Appbar, useTheme} from 'react-native-paper';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import {Link, Tabs, useNavigation} from 'expo-router';
import Colors from 'common/constants/colors';

function TabBarIcon(props: {
  name: React.ComponentProps<typeof FontAwesome>['name'];
  color: string;
}) {
  return <FontAwesome size={28} style={{marginBottom: -3}} {...props} />;
}

export default function TabLayout() {
  const colorScheme = useColorScheme();
  const {colors} = useTheme();
  const navigation = useNavigation();

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
      }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Tab One',
          tabBarIcon: ({color}) => <TabBarIcon name="code" color={color} />,
          header: () => (
            <Appbar.Header style={{backgroundColor: colors.primary}}>
              {navigation.canGoBack() ? (
                <Appbar.BackAction onPress={navigation.goBack} />
              ) : null}
              <Appbar.Content title={'Tab One'} />
            </Appbar.Header>
          ),
          headerRight: () => (
            <Link href="/modal" asChild>
              <Pressable>
                {({pressed}) => (
                  <FontAwesome
                    name="info-circle"
                    size={25}
                    color={Colors[colorScheme ?? 'light'].text}
                    style={{marginRight: 15, opacity: pressed ? 0.5 : 1}}
                  />
                )}
              </Pressable>
            </Link>
          ),
        }}
      />
      <Tabs.Screen
        name="two"
        ...
expo-router-appbar-memo
ヘッダーに <Appbar /> を適用した結果

<Menu /> をヘッダーに追加する

  • Line 5,21-23,41-68
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/(tabs)/_layout.tsx
import * as React from 'react';
import {Pressable, useColorScheme} from 'react-native';
import {Appbar, useTheme, Menu} from 'react-native-paper';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import {Link, Tabs, useNavigation} from 'expo-router';
import Colors from 'common/constants/colors';
function TabBarIcon(props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) {
return <FontAwesome size={28} style={{marginBottom: -3}} {...props} />;
}
export default function TabLayout() {
const colorScheme = useColorScheme();
const {colors} = useTheme();
const navigation = useNavigation();
const [visible, setVisible] = React.useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
}}>
<Tabs.Screen
name="index"
options={{
title: 'Tab One',
tabBarIcon: ({color}) => <TabBarIcon name="code" color={color} />,
header: () => (
<Appbar.Header style={{backgroundColor: colors.primary}}>
{navigation.canGoBack() ? (
<Appbar.BackAction onPress={navigation.goBack} />
) : null}
<Appbar.Content title={'Tab One'} />
{!navigation.canGoBack() ? (
<Menu
visible={visible}
onDismiss={closeMenu}
anchor={
<Appbar.Action icon="dots-vertical" onPress={openMenu} />
}>
<Menu.Item
onPress={() => {
console.log('Option 1 was pressed');
}}
title="Option 1"
/>
<Menu.Item
onPress={() => {
console.log('Option 2 was pressed');
}}
title="Option 2"
/>
<Menu.Item
onPress={() => {
console.log('Option 3 was pressed');
}}
title="Option 3"
disabled
/>
</Menu>
) : null}
</Appbar.Header>
),
headerRight: () => (
<Link href="/modal" asChild>
<Pressable>
{({pressed}) => (
<FontAwesome
name="info-circle"
size={25}
color={Colors[colorScheme ?? 'light'].text}
style={{marginRight: 15, opacity: pressed ? 0.5 : 1}}
/>
)}
</Pressable>
</Link>
),
}}
/>
<Tabs.Screen
name="two"
...
// app/(tabs)/_layout.tsx import * as React from 'react'; import {Pressable, useColorScheme} from 'react-native'; import {Appbar, useTheme, Menu} from 'react-native-paper'; import FontAwesome from '@expo/vector-icons/FontAwesome'; import {Link, Tabs, useNavigation} from 'expo-router'; import Colors from 'common/constants/colors'; function TabBarIcon(props: { name: React.ComponentProps<typeof FontAwesome>['name']; color: string; }) { return <FontAwesome size={28} style={{marginBottom: -3}} {...props} />; } export default function TabLayout() { const colorScheme = useColorScheme(); const {colors} = useTheme(); const navigation = useNavigation(); const [visible, setVisible] = React.useState(false); const openMenu = () => setVisible(true); const closeMenu = () => setVisible(false); return ( <Tabs screenOptions={{ tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint, }}> <Tabs.Screen name="index" options={{ title: 'Tab One', tabBarIcon: ({color}) => <TabBarIcon name="code" color={color} />, header: () => ( <Appbar.Header style={{backgroundColor: colors.primary}}> {navigation.canGoBack() ? ( <Appbar.BackAction onPress={navigation.goBack} /> ) : null} <Appbar.Content title={'Tab One'} /> {!navigation.canGoBack() ? ( <Menu visible={visible} onDismiss={closeMenu} anchor={ <Appbar.Action icon="dots-vertical" onPress={openMenu} /> }> <Menu.Item onPress={() => { console.log('Option 1 was pressed'); }} title="Option 1" /> <Menu.Item onPress={() => { console.log('Option 2 was pressed'); }} title="Option 2" /> <Menu.Item onPress={() => { console.log('Option 3 was pressed'); }} title="Option 3" disabled /> </Menu> ) : null} </Appbar.Header> ), headerRight: () => ( <Link href="/modal" asChild> <Pressable> {({pressed}) => ( <FontAwesome name="info-circle" size={25} color={Colors[colorScheme ?? 'light'].text} style={{marginRight: 15, opacity: pressed ? 0.5 : 1}} /> )} </Pressable> </Link> ), }} /> <Tabs.Screen name="two" ...
// app/(tabs)/_layout.tsx

import * as React from 'react';
import {Pressable, useColorScheme} from 'react-native';
import {Appbar, useTheme, Menu} from 'react-native-paper';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import {Link, Tabs, useNavigation} from 'expo-router';
import Colors from 'common/constants/colors';

function TabBarIcon(props: {
  name: React.ComponentProps<typeof FontAwesome>['name'];
  color: string;
}) {
  return <FontAwesome size={28} style={{marginBottom: -3}} {...props} />;
}

export default function TabLayout() {
  const colorScheme = useColorScheme();
  const {colors} = useTheme();
  const navigation = useNavigation();
  const [visible, setVisible] = React.useState(false);
  const openMenu = () => setVisible(true);
  const closeMenu = () => setVisible(false);

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
      }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Tab One',
          tabBarIcon: ({color}) => <TabBarIcon name="code" color={color} />,
          header: () => (
            <Appbar.Header style={{backgroundColor: colors.primary}}>
              {navigation.canGoBack() ? (
                <Appbar.BackAction onPress={navigation.goBack} />
              ) : null}
              <Appbar.Content title={'Tab One'} />
              {!navigation.canGoBack() ? (
                <Menu
                  visible={visible}
                  onDismiss={closeMenu}
                  anchor={
                    <Appbar.Action icon="dots-vertical" onPress={openMenu} />
                  }>
                  <Menu.Item
                    onPress={() => {
                      console.log('Option 1 was pressed');
                    }}
                    title="Option 1"
                  />
                  <Menu.Item
                    onPress={() => {
                      console.log('Option 2 was pressed');
                    }}
                    title="Option 2"
                  />
                  <Menu.Item
                    onPress={() => {
                      console.log('Option 3 was pressed');
                    }}
                    title="Option 3"
                    disabled
                  />
                </Menu>
              ) : null}
            </Appbar.Header>
          ),
          headerRight: () => (
            <Link href="/modal" asChild>
              <Pressable>
                {({pressed}) => (
                  <FontAwesome
                    name="info-circle"
                    size={25}
                    color={Colors[colorScheme ?? 'light'].text}
                    style={{marginRight: 15, opacity: pressed ? 0.5 : 1}}
                  />
                )}
              </Pressable>
            </Link>
          ),
        }}
      />
      <Tabs.Screen
        name="two"
        ...
expo-router-appbar-menu-icon
ヘッダーに<Menu /> メニューを追加し
expo-router-appbar-menu-open
そのメニューの中身が表示されるか確認をする

おわりに

これで、React Native Paper + React Navigationと同じ環境で最小限の開発環境が作成されました。

※追記:設定の詳細記事はこちら(⬇︎)

スポンサーリンク

コメント

タイトルとURLをコピーしました