アプリ開発を楽しむ【#16:Mapページをつくる】React+Redux+TypeScript
前回までの記事 アプリ開発を楽しむ【#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:カスタムフック】 アプリ開発を楽しむ【#15:ルーティングの設定】
前回の投稿から少し間が空いてしまいましたが、今回は、Mapのページをつくっていきたいと思います。
完成イメージは、次のとおりです。
Map上のマーカーは、投稿(おすすめスポット)されたロケーションを表示しています。
今回やること
1.サンプルデータ(おすすめスポット)を作成する。 2.Store上で投稿リストを管理できるようにする。 3.Mapページ用のコンポーネントを作成する。 では、始めていきます。
1.サンプルデータの作成
①サンプルデータを作成する前に投稿情報の(post)の型定義をしておきます。 frontend/app/src/types/post.tsを次のようにつくります。
import type { Togo } from './togo';
export type Post = {
title: string;
description: string;
publishedAt: string;
image: string;
togo: Togo;
};
②frontend/app/src/sampleData/posts.tsを次のようにつくります。
import type { Post } from '../types/post';
const samplePostList: Post[] = [
{
title: '天空都市 マチュピチュ',
description:
'マチュピチュはアンデスの山中、標高2,400mの断崖に突如として姿をあらわす都市遺跡である。ケチャ語で「年老いた峰」という意味を持つこの地は、15世紀半ばのインカ帝国時代に築かれ1911年、偶然に発見されるまで、深い密林に覆われていた。 そのため神殿、大広場、段々畑、墓地、水路や通路が巡らされた住居跡などがそのままの状態で残されている。',
publishedAt: '2022-01-01',
image: 'https://source.unsplash.com/HSAItzUiSrg',
togo: {
id: 0,
done: true,
tag: '世界遺産',
location: 'マチュピチュ',
position: {
lat: -13.163113012711815,
lng: -72.54494397376752,
},
},
},
{
title: 'あっさり系 豚骨らーめん',
description:
'豚骨は鶏のろっ骨である「鶏ガラ」とともにスープ作りに必要な素材のひとつです。多くのラーメン屋では、豚骨や鶏ガラ、野菜などを加えてスープを作っています。',
publishedAt: '2021-12-01',
image: 'https://source.unsplash.com/mE6kjov4rTg',
togo: {
id: 1,
done: true,
location: 'ラーメン屋',
tag: 'らーめん',
position: {
lat: 35.6808610662155,
lng: 139.76856460990368,
},
},
},
{
title: 'ニューヨークに行ってきました!!',
description:
'ニューヨーク市はハドソン川と大西洋の接点にあり、5 つの行政区に分かれています。その中核となるマンハッタンは、人口密度の高い、世界の主要な商業、金融、文化の中心地の 1 つです。',
publishedAt: '2021-12-01',
image: 'https://source.unsplash.com/iANyXOzpsMM',
togo: {
id: 2,
done: true,
location: 'ニューヨーク',
tag: 'アメリカ',
position: {
lat: 40.75334800401968,
lng: -73.99070313667248,
},
},
},
{
title: '山を登ってきました!',
description:
'飛騨山脈(北アルプス)北部の立山連峰にある標高2,999mの山。富山県の上市町と立山町にまたがる。中部山岳国立公園内にあり、山域はその特別保護地区になっている。日本百名山および新日本百名山に選定されている。',
publishedAt: '2022-04-01',
image: 'https://source.unsplash.com/ePpaQC2c1xA',
togo: {
id: 3,
done: true,
location: '剱岳(つるぎだけ)',
tag: '山',
position: {
lat: 36.624476367402565,
lng: 137.61765903902852,
},
},
},
];
export default samplePostList;
2.StoreでpostListを管理
①frontend/app/src/redux/postSlice.tsを次のようにつくります。
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { Post } from '../types/post';
type PostState = {
postList: Post[];
};
export const initialState: PostState = {
postList: [],
};
export const postSlice = createSlice({
name: 'post',
initialState,
reducers: {
getPostList(state: PostState, action: PayloadAction<Post[]>) {
state.postList = action.payload;
},
},
});
export const { getPostList } = postSlice.actions;
②frontend/app/src/redux/store.tsを修正します。
import { configureStore } from '@reduxjs/toolkit';
// ここから追加
import { postSlice } from './postSlice';
// ここまで追加
import { togoSlice } from './togoSlice';
export const store = configureStore({
reducer: {
togo: togoSlice.reducer,
// ここから追加
post: postSlice.reducer,
// ここまで追加
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
3.Mapコンポーネントの作成
①frontend/app/arc/components/Map.tsxを次のように修正します。
import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { Marker, InfoWindow } from '@react-google-maps/api';
import { RootState, AppDispatch } from '../redux/store';
import { getPostList, initialState } from '../redux/postSlice';
import GMap from './common/GMap';
import type { Post } from '../types/post';
import type { MapPosition } from '../types/map';
import samplePostList from '../sampleData/posts';
const Map = () => {
const mapStyles = {
height: '100%',
width: '100%',
};
const mapOptions = {
gestureHandling: 'cooperative',
zoomControl: true,
scaleControl: true,
streetViewControl: false,
panControl: false,
mapTypeControl: true,
fullscreenControl: true,
};
const initialMapCenterPosition = {
lat: 35.6808610662155,
lng: 139.76856460990368,
};
const dispatch: AppDispatch = useDispatch();
const { postList } = useSelector((state: RootState) => state.post || initialState);
const [activeMarker, setActiveMarker] = useState<number | null>(null);
const [mapCenterPosition, setMapCenterPosition] = useState<MapPosition>(initialMapCenterPosition);
useEffect(() => {
dispatch(getPostList(samplePostList));
}, [dispatch]);
const handleActiveMarker = (post: Post) => {
if (post.togo.id === undefined || post.togo.id === activeMarker) {
return;
}
setActiveMarker(post.togo.id);
};
return (
<GMap
mapStyles={mapStyles}
zoom={2}
mapCenterPosition={mapCenterPosition}
mapOptions={mapOptions}
handleClick={() => setActiveMarker(null)}
>
{postList &&
postList.map((post) => (
<Marker
key={post.togo.id}
position={post.togo.position}
onClick={() => handleActiveMarker(post)}
>
{activeMarker === post.togo.id ? (
<InfoWindow onCloseClick={() => setActiveMarker(null)}>
<Link to={`/posts/${post.togo.id}`}>{post.togo.location}</Link>
</InfoWindow>
) : null}
</Marker>
))}
</GMap>
);
};
export default Map;
この部分で、マーカーをクリックしたときに吹き出しがでるようにしています。
{activeMarker === post.togo.id ? (
<InfoWindow onCloseClick={() => setActiveMarker(null)}>
<Link to={`/posts/${post.togo.id}`}>{post.togo.location}</Link>
</InfoWindow>
) : null}
②frontend/app/src/components/common/GMap.tsxを修正します。
~~~省略~~~
type GMapProps = {
children?: React.ReactNode;
mapStyles: { height: string; width: string };
zoom: number;
mapCenterPosition: { lat: number; lng: number };
mapOptions: {
gestureHandling: string;
zoomControl: boolean;
scaleControl: boolean;
streetViewControl: boolean;
panControl: boolean;
mapTypeControl: boolean;
fullscreenControl: boolean;
};
// ここから修正
handleClick?: ((e: google.maps.MapMouseEvent) => void) | (() => void);
// ここまで修正
};
~~~省略~~~
今日はここまでです。
コードはGitHubに置いてありますのでよければ参考にしてください。 mainブランチは常に最新のものになります。 今回の内容はblog_16のブランチを参照してください。 https://github.com/KINE-M/togo_app