前回までの記事 アプリ開発を楽しむ【#1:アプリの概要】 アプリ開発を楽しむ【#2:環境構築1(React+TypeScript)】 アプリ開発を楽しむ【#3:環境構築2 (ESLint+Prettier)】 アプリ開発を楽しむ【#4:ヘッダー】 アプリ開発を楽しむ【#5:MyTogoList】 アプリ開発を楽しむ【#6:Reduxで状態管理1】 アプリ開発を楽しむ【#7:Reduxで状態管理2】 アプリ開発を楽しむ【#8:Google Map API】 アプリ開発を楽しむ【#9:新規追加モーダルUI(togo)】 アプリ開発を楽しむ【#10:Google Mapから座標を取得】 アプリ開発を楽しむ【#11:Togoの追加】 アプリ開発を楽しむ【#12:Togoの編集1】 アプリ開発を楽しむ【#13:Togoの編集2】 アプリ開発を楽しむ【#14:カスタムフック】

今回はルーティングの設定をやっていきたいと思います。

ここでいうルーティングの設定とは、React Routerというライブラリーを使ってページ遷移をできるようにするということです。

余談ですが、僕なんかは、ルーティングの設定というと、ルーターとかのルーティングテーブルの設定をイメージしてしまいますが。。。

今回の目標は、ナビゲーションバーにあるMYLIST、POSTS、MAP、Loginをそれぞれクリックすると、そのページに遷移ができるようになるまでとなります。

出来上がりイメージは、以下のとおりです。 スクリーンショット 2022-09-18 23.18.52.png

スクリーンショット 2022-09-18 23.18.58.png

スクリーンショット 2022-09-18 23.19.03.png

スクリーンショット 2022-09-18 23.19.20.png

スクリーンショット 2022-09-18 23.19.37.png

URLに応じて、該当のページを表示できるようにしていきます。 /posts/11は、postのidが11のpostを詳細表示できるページを想定しています。(postは、ユーザーが行ってきてよかったところを投稿するデータを想定しています。まだ、この機能は実装していません。) /post/abcなど、想定していないURLの場合は、404 Not Foundを表示するようにします。

React Routerのライブラリーは、 環境構築【第1回】でインストールしています。

1.ページ用のコンポーネントを作成

①AllPostsコンポーネントを作成 frontend/app/src/components/AllPosts.tsxを次のように作成します。

import Container from '@mui/material/Container';

const AllPosts = () => (
  <Container maxWidth="lg" sx={{ pt: 2 }}>
    All Posts
  </Container>
);

export default AllPosts;

②Mapコンポーネントを作成 frontend/app/src/components/Map.tsxを次のように作成します。

import Container from '@mui/material/Container';

const Map = () => (
  <Container maxWidth="lg" sx={{ pt: 2 }}>
    Map
  </Container>
);

export default Map;

③Loginコンポーネントを作成 frontend/app/src/components/Login.tsxを次のように作成します。

import Container from '@mui/material/Container';

const Login = () => (
  <Container maxWidth="lg" sx={{ pt: 2 }}>
    Login
  </Container>
);

export default Login;

④OnePostコンポーネントを作成 frontend/app/src/components/OnePost.tsxを次のように作成します。

import { Container } from '@mui/material';
import { useParams } from 'react-router-dom';

const OnePost = () => {
  const { id } = useParams();

  return (
    <Container maxWidth="lg" sx={{ pt: 2 }}>
      One Post {id}
    </Container>
  );
};

export default OnePost;

React Routerの機能のuseParamsを使って、URLのidを取得しています。

⑤Errorコンポーネントを作成 frontend/app/src/components/Error.tsxを次のように作成します。

import Container from '@mui/material/Container';

const Error = () => (
  <Container maxWidth="lg" sx={{ pt: 2 }}>
    404 Not Found
  </Container>
);

export default Error;

⑥frontend/app/src/components/Home.tsxを修正します。

import { Container, Paper } from '@mui/material';
import MyTogoList from './togo/MyTogoList';

const Home = () => (

  // ここから修正
  <Container maxWidth="lg" sx={{ pt: 2 }}>
  // ここまで修正

    <Paper sx={{ p: 2, display: 'flex', flexDirection: 'column', position: 'relative' }}>
      <MyTogoList />
    </Paper>
  </Container>
);

export default Home;

cssを修正しています。

2.Layoutコンポーネントの作成

frontend/app/src/components/layout/Layout.tsxを次のように作成します。

import { Outlet } from 'react-router-dom';
import { Box } from '@mui/material';
import Header from './Header';

const Layout = () => (
  <>
    <Header />
    <Box
      component="main"
      sx={{
        backgroundColor: (theme) =>
          theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900],
        flexGrow: 1,
        height: '100vh',
        overflow: 'auto',
        pt: { xs: '56px', sm: '64px', md: '70px' },
      }}
    >
      <Outlet />
    </Box>
  </>
);

export default Layout;

Layoutコンポーネントは、App.tsxの一部を切り出してつくっています。 Outletコンポーネントは、React Routerの機能で、この部分にそれぞれのページが差し込まれます。

3.App.tsxの修正

frontend/app/src/App.tsxを次のように修正します。

import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { Box, CssBaseline } from '@mui/material';
import AllPosts from './components/AllPosts';
import Error from './components/Error';
import Home from './components/Home';
import Layout from './components/layout/Layout';
import Login from './components/Login';
import OnePost from './components/OnePost';
import Map from './components/Map';

const App = () => (
  <Box sx={{ display: 'flex' }}>
    <CssBaseline />
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="/posts" element={<AllPosts />} />
          <Route path="/posts/:id" element={<OnePost />} />
          <Route path="/map" element={<Map />} />
          <Route path="/login" element={<Login />} />
          <Route path="*" element={<Error />} />
        </Route>
      </Routes>
    </BrowserRouter>
  </Box>
);

export default App;

4.Headerコンポーネントの修正

import * as React from 'react';

// ここから追加
import { useNavigate, Link } from 'react-router-dom';
// ここまで追加

import {
  AppBar,
  Box,
  Button,
  Container,
  IconButton,

  // ここから削除
  Link,
  // ここまで削除

  Toolbar,
  Typography,
  Menu,
  MenuItem,
} from '@mui/material';
import { Menu as MenuIcon, LocationOn as LocationOnIcon } from '@mui/icons-material';

const pages = ['MyList', 'Posts', 'Map'];
const settings = ['Account', 'Logout'];

const Header = () => {

  // ここから追加
  const navigation = useNavigate();
  // ここまで追加

  const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(null);
  const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(null);

  const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorElNav(event.currentTarget);
  };

  const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorElUser(event.currentTarget);
  };

  const handleCloseNavMenu = () => {
    setAnchorElNav(null);
  };

  const handleCloseUserMenu = () => {
    setAnchorElUser(null);
  };

  // ここから追加
  const handleOpenLink = (page: string) => {
    const link: string = page.toLowerCase() === 'mylist' ? '/' : `/${page.toLowerCase()}`;
    navigation(link);
    if (anchorElNav !== null) {
      handleCloseNavMenu();
    }
  };
  // ここまで追加

  return (
    <AppBar position="absolute">
      <Container maxWidth="xl">
        <Toolbar disableGutters>
          <LocationOnIcon sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
          <Typography
            variant="h6"
            noWrap
            component="a"
            href="/"
            sx={{
              mr: 2,
              display: { xs: 'none', md: 'flex' },
              fontFamily: 'monospace',
              fontWeight: 700,
              letterSpacing: '.1rem',
              color: 'inherit',
              textDecoration: 'none',
            }}
          >
            TOGO LIST
          </Typography>

          <Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none' } }}>
            <IconButton
              size="large"
              aria-label="account of current user"
              aria-controls="menu-appbar"
              aria-haspopup="true"
              onClick={handleOpenNavMenu}
              color="inherit"
            >
              <MenuIcon />
            </IconButton>
            <Menu
              id="menu-appbar"
              anchorEl={anchorElNav}
              anchorOrigin={{
                vertical: 'bottom',
                horizontal: 'left',
              }}
              keepMounted
              transformOrigin={{
                vertical: 'top',
                horizontal: 'left',
              }}
              open={Boolean(anchorElNav)}
              onClose={handleCloseNavMenu}
              sx={{
                display: { xs: 'block', md: 'none' },
              }}
            >
              {pages.map((page) => (

                // ここから修正
                <MenuItem key={page} onClick={() => handleOpenLink(page)}>
                // ここまで修正

                  <Typography textAlign="center">{page}</Typography>
                </MenuItem>
              ))}
            </Menu>
          </Box>
          <LocationOnIcon sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} />
          <Typography
            variant="h5"
            noWrap
            component="a"
            href=""
            sx={{
              mr: 2,
              display: { xs: 'flex', md: 'none' },
              flexGrow: 1,
              fontFamily: 'monospace',
              fontWeight: 700,
              letterSpacing: '.1rem',
              color: 'inherit',
              textDecoration: 'none',
            }}
          >
            TOGO LIST
          </Typography>
          <Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
            {pages.map((page) => (

              // ここから修正
              <Button
                key={page}
                sx={{ my: 2, color: 'white', fontSize: '15px', display: 'block' }}
                onClick={() => handleOpenLink(page)}
              >
              // ここまで修正

                {page}
              </Button>
            ))}
          </Box>

          <Box sx={{ flexGrow: 0 }}>

            // ここから修正
            <Link style={{ textDecoration: 'none', color: 'white' }} to="/login">
            // ここまで修正

              Login
            </Link>
            <Menu
              sx={{ mt: '45px' }}
              id="menu-appbar"
              anchorEl={anchorElUser}
              anchorOrigin={{
                vertical: 'top',
                horizontal: 'right',
              }}
              keepMounted
              transformOrigin={{
                vertical: 'top',
                horizontal: 'right',
              }}
              open={Boolean(anchorElUser)}
              onClose={handleCloseUserMenu}
            >
              {settings.map((setting) => (
                <MenuItem key={setting} onClick={handleCloseUserMenu}>
                  <Typography textAlign="center">{setting}</Typography>
                </MenuItem>
              ))}
            </Menu>
          </Box>
        </Toolbar>
      </Container>
    </AppBar>
  );
};

export default Header;

MYLIST、POSTS、MAPページへの移動は、React Router のuseNavigateを使っています。 Loginページへの移動は、React Router のLinkコンポーネントを使っています。 以上でページ遷移ができるようになりました。

今回はここまでです。

コードはGitHubに置いてありますのでよければ参考にしてください。 mainブランチは常に最新のものになります。 今回の内容はblog_15のブランチを参照してください。 https://github.com/KINE-M/togo_app