アプリ開発を楽しむ【#15:ルーティングの設定】
前回までの記事 アプリ開発を楽しむ【#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をそれぞれクリックすると、そのページに遷移ができるようになるまでとなります。
出来上がりイメージは、以下のとおりです。
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