[react]react-queryを使用してポケモン図2を作成
今ポケモン図鑑APIを取ります
PokeAPI DOC
types/index.ts
APIのバインドを開始します.
hooks/usePokemon
components/PokemonList.tsx
次に、ポケモンの詳細を表示できるページを作成します.その前にutils/index.TSファイルを生成し、ポケモンの色、タイプ、インデックスに基づいて16進コードに変換し、フォーマットコードを記述します.
utils/index.ts
components/PokemonInfo.tsx
components/Tabs
pages/DetailPage
PokeAPI DOC
types/index.ts
export type Sprites = {
back_default: string;
back_shiny: string;
front_default: string;
front_shiny: string;
other: {
dream_world: {
front_default: string;
};
"official-artwork": {
front_default: string;
};
};
};
export type Type = {
slot: number;
type: {
name: string;
url: string;
};
};
export type Stat = {
base_stat: number;
effort: number;
stat: {
name: string;
url: string;
};
};
export type Ability = {
ability: {
name: string;
url: string;
};
is_hidden: boolean;
slot: number;
};
export type Language = {
name: string;
url: string;
};
export type Name = {
language: Language;
name: string;
};
export type Color = {
name: string;
url: string;
};
export type Version = {
name: string;
url: string;
};
export type FlavorTextEntry = {
flavor_text: string;
language: Language;
version: Version;
};
export type GrowthRate = {
name: string;
url: string;
};
export type EffectEntry = {
effect: string;
language: Language;
short_effect: string;
};
export type SimplePokemonInfo = {
name: string;
url: string;
};
export type DamageRelation = {
double_damage_from: Array<{ name: string; url: string }>;
double_damage_to: Array<{ name: string; url: string }>;
half_damage_from: Array<{ name: string; url: string }>;
half_damage_to: Array<{ name: string; url: string }>;
};
export type EvolutionDetail = {
min_level: number;
trigger: {
name: string;
url: string;
};
};
export type Chain = {
is_baby: boolean;
evolution_details: Array<EvolutionDetail>;
evolves_to: Array<EvolutionTo>;
species: {
name: string;
url: string;
};
};
export type EvolutionTo = {
evolution_details: Array<EvolutionDetail>;
is_baby: boolean;
evolves_to: Array<EvolutionTo>;
species: {
name: string;
url: string;
};
};
export type ListResponse = {
count: number;
results: Array<SimplePokemonInfo>;
};
export type PokemonResponse = {
id: number;
name: string;
order: number;
sprites: Sprites;
base_experience: number;
height: number;
weight: number;
stats: Array<Stat>;
abilities: Array<Ability>;
types: Array<Type>;
};
export type SpeciesResponse = {
id: number;
name: string;
order: number;
names: Array<Name>;
color: Color;
flavor_text_entries: Array<FlavorTextEntry>;
growth_rate: GrowthRate;
gender_rate: number;
is_legendary: boolean;
is_mythical: boolean;
evolution_chain: {
url: string;
};
};
export type AbilityResponse = {
id: number;
name: string;
names: Array<Name>;
is_main_series: boolean;
effect_entries: Array<EffectEntry>;
};
export type TypeResponse = {
id: number;
name: string;
damage_relations: DamageRelation;
};
export type EvolutionChainResponse = {
id: number;
chain: Chain;
};
usePokemon Custom Hookの作成
APIのバインドを開始します.
src
ディレクトリにhooks
ディレクトリを作成した後、ポケモンリストにインポートされたusePokemon
hookを作成します.hooks/usePokemon
import axios, { AxiosResponse } from "axios";
import { useQuery } from "react-query";
import { UseQueryResult } from "react-query/types/react/types";
// import { PokemonResponse } from '../types';
const pokemonApi = (id?: string) =>
axios.get(`https://pokeapi.co/api/v2/pokemon/${id || ""}`, {
params: { limit: 151 }, // 1세대 포켓몬이 151마리였다.
});
const usePokemon = <T>(id?: string): UseQueryResult<AxiosResponse<T>, Error> =>
useQuery(id ? ["pokemon", id] : "pokemon", () => pokemonApi(id));
export default usePokemon;
usePokemon
は、id
を因子として、UseQueryResult
を返すhookである.バインドAPI
components/PokemonList.tsx
import React from "react";
import styled from "@emotion/styled";
import usePokemon from "../hooks/usePokemon";
import { ListResponse } from "../types/indes";
const Base = styled.div`
margin-top: 24px;
`;
const LoadingWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: calc(100vh - 180px);
`;
const Loading = styled.img``;
const ListItem = styled.li`
position: relative;
list-style: none;
display: flex;
align-items: center;
box-shadow: 6px 4px 14px 5px rgba(0, 0, 0, 0.21);
border-radius: 12px;
& + & {
margin-top: 18px;
}
`;
const List = styled.ul`
margin: 0;
padding: 0;
`;
const Image = styled.img``;
const Name = styled.p`
margin: 0;
padding: 0 0 0 12px;
flex: 1 1 100%;
color: #374151;
text-transform: capitalize;
font-size: 16px;
font-weight: bold;
`;
const Index = styled.p`
position: absolute;
margin: 0;
padding: 0;
right: 16px;
bottom: 16px;
font-size: 24px;
font-weight: bold;
color: #d1d5db;
`;
const getImageUrl = (index: number): string =>
`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${index}.png`;
const PokemonList: React.FC = () => {
const { isLoading, isError, data } = usePokemon<ListResponse>();
const formatNumbering = (index: number): string => {
return `#${String(index).padStart(3, "0")}`;
};
return (
<Base>
{isLoading || isError ? (
<LoadingWrapper>
<Loading src="/loading.gif" alt="loading" />
</LoadingWrapper>
) : (
<List>
{data?.data.results.map((pokemon, idx) => (
<ListItem key={pokemon.name}>
<Image src={getImageUrl(idx + 1)} />
<Name>{pokemon.name}</Name>
<Index>{formatNumbering(idx + 1)}</Index>
</ListItem>
))}
</List>
)}
</Base>
);
};
export default PokemonList;
APIバインドを完了すると、以下の画面が得られます.ポケモン詳細ページの作成
次に、ポケモンの詳細を表示できるページを作成します.その前にutils/index.TSファイルを生成し、ポケモンの色、タイプ、インデックスに基づいて16進コードに変換し、フォーマットコードを記述します.
utils/index.ts
export const mapColorToHex = (color?: string) => {
// 포켓몬의 컬러를 받아 hexCode 로 변
switch (color) {
case "white":
case "gray":
return "#6B7280";
case "brown":
return "#92400E";
case "yellow":
return "#F59E0B";
case "green":
return "#10B981";
case "red":
return "#EF4444";
case "blue":
return "#3B82F6";
case "purple":
return "#8B5CF6";
case "pink":
return "#EC4899";
case "black":
return "#1F2937";
default:
return "#6B7280";
}
};
export const mapTypeToHex = (type?: string) => {
// 포켓몬의 타입를 받아 hexCode 로 변
switch (type) {
case "bug":
return "#92BC2C";
case "dark":
return "#595761";
case "dragon":
return "#0C69C8";
case "electric":
return "#F2D94E";
case "fire":
return "#FBA54C";
case "fairy":
return "#EE90E6";
case "fighting":
return "#D3425F";
case "flying":
return "#A1BBEC";
case "ghost":
return "#5F6DBC";
case "grass":
return "rgba(5, 150, 105, 1)";
case "ground":
return "#DA7C4D";
case "ice":
return "#75D0C1";
case "normal":
return "#A0A29F";
case "poison":
return "#B763CF";
case "psychic":
return "#FA8581";
case "rock":
return "#C9BB8A";
case "steel":
return "#5695A3";
case "water":
return "#539DDF";
default:
return "#6B7280";
}
};
export const formatNumbering = (
pokemonIndex: number | string
): string => // 포켓몬의 인덱스를 받아서 #001 형태로 변
`#${(typeof pokemonIndex === "number"
? String(pokemonIndex)
: pokemonIndex
).padStart(3, "0")}`;
次はComponent/PokemonInfoです.tsxファイルを作成します.components/PokemonInfo.tsx
import React from "react";
import styled from "@emotion/styled/macro";
import { Color, Type } from "../types";
import { formatNumbering, mapColorToHex, mapTypeToHex } from "../utils";
type Props = {
id: string;
name?: string;
types?: Array<Type>;
color?: Color;
};
const Base = styled.div<{ color?: string }>`
display: flex;
flex-direction: column;
background-color: ${({ color }) => color};
padding: 20px;
border-bottom-left-radius: 20%;
border-bottom-right-radius: 20%;
`;
const ThumbnailImageWrapper = styled.div`
width: 160px;
margin-inline: auto;
margin-block: 24px;
`;
const ThumbnailImage = styled.img`
width: 100%;
height: 100%;
object-fit: contain;
`;
const InfoWrapper = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
`;
const Name = styled.div`
color: #fff;
font-size: 30px;
font-weight: bold;
text-transform: capitalize;
`;
const Index = styled.div`
color: #fff;
font-size: 36px;
font-weight: bold;
opacity: 0.75;
`;
const TypeWrapper = styled.div<{ color: string }>`
background-color: ${({ color }) => color};
padding: 4px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
`;
const TypeList = styled.div`
display: flex;
margin-top: 8px;
${TypeWrapper} + ${TypeWrapper} {
margin-left: 8px;
}
`;
const TypeInfo = styled.img`
height: 12px;
`;
const ImageWrapper = styled.div`
position: absolute;
width: 288px;
height: 288px;
left: -96px;
top: -96px;
opacity: 0.75;
`;
const Image = styled.img`
width: 100%;
height: 100%;
object-fit: contain;
`;
const PokemonInfo: React.FC<Props> = ({ id, name, color, types }) => (
<Base color={mapColorToHex(color?.name)}>
<ImageWrapper>
<Image src="/assets/pocketball.svg" />
</ImageWrapper>
<InfoWrapper>
<Name>{name}</Name>
<Index>{formatNumbering(id)}</Index>
</InfoWrapper>
<TypeList>
{types?.map(({ type }, idx) => (
<TypeWrapper key={idx} color={mapTypeToHex(type.name)}>
<TypeInfo src={`/assets/${type.name}.svg`} />
</TypeWrapper>
))}
</TypeList>
<ThumbnailImageWrapper>
<ThumbnailImage
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/dream-world/${id}.svg`}
alt="image"
/>
</ThumbnailImageWrapper>
</Base>
);
export default PokemonInfo;
次に、以下の手順に従って、自分が望むポケモン情報を選択できるタグセットを作成します.components/Tabs
import React from "react";
import styled from "@emotion/styled/macro";
import { Color } from "../types";
import { mapColorToHex } from "../utils";
type Props = {
tab: "about" | "stats" | "evolution";
onClick: (tab: "about" | "stats" | "evolution") => void;
color?: Color;
};
const List = styled.ul`
list-style: none;
margin: 0;
padding: 0;
display: flex;
`;
const ListItem = styled.li`
& + & {
margin-left: 16px;
}
`;
const TabButton = styled.button<{ active?: boolean; color: string }>`
margin: 0;
border-radius: 8px;
box-shadow: 6px 4px 14px 5px rgba(0, 0, 0, 0.21);
padding: 6px 12px;
background-color: #fff;
border: none;
font-size: 16px;
color: ${({ active, color }) => (active ? color : "#6B7280")};
`;
const Tabs: React.FC<Props> = ({ tab, onClick, color }) => (
<List>
<ListItem onClick={() => onClick("about")}>
<TabButton active={tab === "about"} color={mapColorToHex(color?.name)}>
About
</TabButton>
</ListItem>
<ListItem onClick={() => onClick("stats")}>
<TabButton active={tab === "stats"} color={mapColorToHex(color?.name)}>
Stats
</TabButton>
</ListItem>
<ListItem onClick={() => onClick("evolution")}>
<TabButton
active={tab === "evolution"}
color={mapColorToHex(color?.name)}
>
Evolution
</TabButton>
</ListItem>
</List>
);
export default Tabs;
それからDetailPageに入って、ラベルが正常かどうかを見ます.ラベルが正常に動作しているかどうかは、color
の値をランダムに入力します.pages/DetailPage
import React, { useState } from "react";
import { useParams } from "react-router-dom";
import Tabs from "../components/Tabs";
type Parmas = {
id: string;
};
type Tab = "about" | "stats" | "evolution";
const DetailPage: React.FC = () => {
const { id } = useParams<Parmas>();
const [selectedTab, setSelectedTab] = useState<Tab>("about");
const handleClick = (tab: Tab) => {
setSelectedTab(tab);
};
return (
<div>
<Tabs
tab={selectedTab}
onClick={handleClick}
color={{ name: "red", url: "" }}
/>
</div>
);
};
export default DetailPage;
Reference
この問題について([react]react-queryを使用してポケモン図2を作成), 我々は、より多くの情報をここで見つけました https://velog.io/@shinwonse/React-react-query를-이용한-포켓몬-도감-만들기-2テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol