Expo routerのtabsを使いつつログイン画面は残したい

Expo便利ですよね。

最近[Expo Router](https://docs.expo.dev/versions/latest/sdk/router/)が出てきてReact Navigationとの違いを検証するべく、個人で試していました。

### 以下回想...

ひとまずプロジェクト作成。
```
npx create-expo-app@latest
```
名前はsample-appとしました。

ディレクトリはこんな感じ。

```
└── sample-app
    app
    ├── (tabs)
    │   ├── _layout.tsx
    │   ├── explore.tsx
    │   └── index.tsx
    ├── +not-found.tsx
    └── _layout.tsx
    ├── app.json
    ├── assets
    ├── components
    ├── constants
    ├── hooks
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── scripts
    ├── README.md
    └── tsconfig.json
    
```
なるほど。
_layout.tsxが二つあるのか...

```
npm run ios
```

![スクリーンショット 2024-12-11 15.30.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305682/1d79aba3-5491-5701-1ff8-8abc7d17226a.png)

いえーい


なるほどなるほど
`(tabs)`の中にページ作ればいいのねー
https://docs.expo.dev/router/advanced/tabs/


```app/(tabs)/setting.tsx
import { Image, StyleSheet } from 'react-native';

import ParallaxScrollView from '@/components/ParallaxScrollView';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';

export default function SettingScreen() {
  return (
    <ParallaxScrollView
      headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
      headerImage={
        <Image
          source={require('@/assets/images/partial-react-logo.png')}
          style={styles.reactLogo}
        />
      }>
      <ThemedView style={styles.titleContainer}>
        <ThemedText type="title">Setting</ThemedText>
      </ThemedView>
    </ParallaxScrollView>
  );
}

const styles = StyleSheet.create({
  titleContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
  },
  stepContainer: {
    gap: 8,
    marginBottom: 8,
  },
  reactLogo: {
    height: 178,
    width: 290,
    bottom: 0,
    left: 0,
    position: 'absolute',
  },
});
```

```app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import React from 'react';
import { Platform } from 'react-native';
import FontAwesome from '@expo/vector-icons/FontAwesome';

import { HapticTab } from '@/components/HapticTab';
import { IconSymbol } from '@/components/ui/IconSymbol';
import TabBarBackground from '@/components/ui/TabBarBackground';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
        headerShown: false,
        tabBarButton: HapticTab,
        tabBarBackground: TabBarBackground,
        tabBarStyle: Platform.select({
          ios: {
            // Use a transparent background on iOS to show the blur effect
            position: 'absolute',
          },
          default: {},
        }),
      }}>
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color }) => <IconSymbol size={28} name="house.fill" color={color} />,
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: 'Explore',
          tabBarIcon: ({ color }) => <IconSymbol size={28} name="paperplane.fill" color={color} />,
        }}
      />
      // 以下を追加
      <Tabs.Screen
        name="setting"
        options={{
          title: 'Setting',
          tabBarIcon: ({ color }) =>  <FontAwesome size={28} name="cog" color={color} />,
        }}
      />
    </Tabs>
  );
}
```


![Simulator Screenshot - iPhone 16 Plus - 2024-12-11 at 17.05.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305682/4b6c9fa0-7c93-8060-4130-b20c65b64ffe.png)

めっちゃ簡単じゃん！！


ん？
でもtabに行く前にログイン画面を用意したい時はどうすればいいんだ？



appの中には_layout.tsxしかないしなー

もしかして

```
app
├── (tabs)
│   ├── _layout.tsx
│   ├── explore.tsx
│   ├── index.tsx
│   └── setting.tsx
├── +not-found.tsx
├── signin.tsx
└── _layout.tsx
```
こうで


こうか！
```app/_layout.tsx
~ 略 ~

export default function RootLayout() {
~ 中略 ~

  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <Stack>
        // 以下を追加
        <Stack.Screen name="signin"/>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="+not-found" />
      </Stack>
      <StatusBar style="auto" />
    </ThemeProvider>
  );
}
```



```shell
 (NOBRIDGE) WARN  [Layout children]: No route named "signin" exists in nested children: ["+not-found", "_sitemap", "(tabs)"] [Component Stack]
```

違うらしい

こうか！

```app/_layout.tsx
~ 略 ~
  const [display, setDisplay] = useState(false);
  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      {display ? (
        <Stack>
          <Stack.Screen name="(tabs)" options={{headerShown: false}} />
          <Stack.Screen name="signin" />
          <Stack.Screen name="+not-found" />
        </Stack>
      ) : (
        <Stack>
          <Stack.Screen name="signin" />
        </Stack>
      )}
      <StatusBar style="auto" />
    </ThemeProvider>
  );
}
```

違うねー
(headerに(tabs)が表示されている)

![Simulator Screenshot - iPhone 16 Plus - 2024-12-13 at 18.23.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305682/78c4f997-28e9-58d7-16bb-7f8830ea014a.png)

### Few Years Later...

んん？
stackのドキュメントには`app/index.tsx`が存在している...
https://docs.expo.dev/router/advanced/stack

`app/(tabs)/index.tsx` -> `app/index.tsx`に移動してみるか

内容はこんな感じで
```app/index.tsx
import { Button, View } from 'react-native';
import {useRouter} from 'expo-router';

export default function HomeScreen() {
  const router = useRouter();

  return (
    <View>
      <Button
        title="Sign In"
        onPress={() => router.push('/signin')}>
      </Button>
      <Button
        title="Sign Un"
        onPress={() => router.push('/signup')}>
      </Button>
    </View>
  );
}
```

sign in ページを作成（内容は適当）
```app/signin.tsx
import React, {useState} from 'react';
import {View, Button} from 'react-native';
import {useRouter} from 'expo-router';

export default function SignIn() {
  const router = useRouter();

  return (
    <View>
      <Button
        title="Sign In"
        onPress={() => router.push('/setting')}>
      </Button>
    </View>
  );
}
```

sign up ページを作成（内容は適当）
```app/signup.tsx
import React, {useState} from 'react';
import {View} from 'react-native';

export default function SignUp() {
  return (
    <View></View>
  );
}

```

ディレクトリは最終的にこんな感じ
```
app
├── (tabs)
│   ├── _layout.tsx
│   ├── explore.tsx
│   └── setting.tsx
├── +not-found.tsx
├── _layout.tsx
├── index.tsx
├── signin.tsx
└── signup.tsx
```

![iPhone 16 Plus Screen Recording Dec 13 2024.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305682/ed816bff-66ca-d7db-1496-983adfddb3f2.gif)

いえーい
うまくいったぜー

### まとめ
`tabs`のドキュメント読んでたらディレクトリ構造が固定じゃないといけないと思ってしまうね。bottom tabを簡単に実装できていい感じ。
https://docs.expo.dev/router/advanced/tabs/#get-started