Router 안에 Router 를 통해
Total Suply 까지는 리 렌더링 되지 않고
아래 Chart나 Price 를 누름에 따라 그 아래 부분만 리렌더링 된다.
정말 멋지지
chart 나 Price를 누름에 따라 URL이 바뀌고 아래 H1이 바뀐다.
이제 tabs Component로 만들어보자.
그리고 어떻게 유저와 소통할지 생각해보자.
선택된 탭이라고 말해주며 소통해야하니..
Tabs Component를 만들어서 grid를 주고
Tab Component를 활용해서 anchor를 갖도록 했다.
쉽게 소통하도록 한 것이다.
특정한 URL에 있는 것을 알려준다.
훅을 만들고
":/coinId/price" 에 있는지 확인가능하다.
useRouteMatch를 console로 확인하면
객체 정보들을 알 수 있다.
그래서 선택한 URL에 들어가 있다면 object를 받는다.
ChartMatch도 마찬가지이다.
props를 추가한다.
URL에 들어와 있다면 객체를 받고, 아니라면 null 을 return 받을 것이다.
style을 바꿔주기 위해
ture 인지 확인하고
style을 적용한다.
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const Header = styled.header`
height: 20vh;
display: flex;
justify-content: center;
align-items: center;
`;
const CoinsList = styled.ul``;
const Coin = styled.li`
background-color: white;
color: ${(props) => props.theme.bgColor};
margin-bottom: 10px;
padding: 20px;
border-radius: 15px;
a {
padding: 5px; // 좀 더 넓은 범위에서 transition 효과 적용 가능
transition: color 0.2s ease-in;
}
&:hover {
a {
color: ${(props) => props.theme.accentColor};
}
// 아래에서는 a가 아닌 Link라는 이름으로 사용했지만
// css에서는 anchor 를 선택해야 했다. 이건 모든 react router link들이
// 결국에는 anchor로 바뀔거기도 하고,
// react router dom이 우리 대신 설정을 도와줄 특별한 event listener들이 있기도 하다
}
`;
const Title = styled.h1`
font-size: 48px;
color: ${(props) => props.theme.accentColor};
`;
const Loader = styled.span`
text-align: center;
display: block;
`;
const Img = styled.img`
width: 25px;
height: 25px;
margin-right: 10px;
`;
interface CoinInterface {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
function Coins() {
//state를 이용해 coins를 만들어 준다
const [coins, setCoins] = useState<CoinInterface[]>([]);
const [loading, setLoading] = useState(true);
//state가 coins으로 이루어진 array라는 것을 알려주기 위해<interface>작성
useEffect(() => {
(async () => {
const response = await fetch('https://api.coinpaprika.com/v1/coins');
const json = await response.json();
setCoins(json.slice(0, 100));
setLoading(false);
// loading 상태 수정하기
})();
}, []);
return (
<Container>
<Header>
<Title>코인</Title>
</Header>
{loading ? (
<Loader>"Loading..."</Loader>
) : (
//loading 이 참이면 Loading... 출력, 거짓이면 CoinsList 보여줌
<CoinsList>
{coins.map((coin) => (
<Coin key={coin.id}>
<Img
src={`https://cryptoicon-api.vercel.app/api/icon/${coin.symbol.toLowerCase()}`}
/>
<Link
to={{
pathname: `/${coin.id}`,
state: { name: coin.name },
//Link를 이용해 string 이외에 더 많은 데이터를 보낼 수 있다
}}
>
{coin.id}
</Link>
</Coin>
))}
</CoinsList>
)}
</Container>
);
}
export default Coins;
import { useEffect, useState } from 'react';
import { Switch, Route, useLocation, useParams, useRouteMatch } from 'react-router';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import Chart from './Chart';
import Price from './Price';
//useParams 는 URL에서 관심있어 하는 정보를 잡아낼 수 있게 해준다.
function Coin() {
const [loading, setLoading] = useState(true);
//state가 coins으로 이루어진 array라는 것을 알려주기 위해<interface>작성
//Coins 에서 보내는 state 를 사용하기 위해 useLocation 이라는 hook을 사용해서 받아와보자! react-router-dom이 보내주는
// location object에 접근하기만 하면 된다.
const { coinId } = useParams<RouteParams>();
const { state } = useLocation<RouteState>();
const priceMatch = useRouteMatch('/:coinId/price');
const chartMatch = useRouteMatch('/:coinId/chart');
useEffect(() => {
(async () => {
const infoData = await (await fetch(`https://api.coinpaprika.com/v1/coins/${coinId}`)).json();
const priceData = await (
await fetch(`https://api.coinpaprika.com/v1/tickers/${coinId}`)
).json();
setInfo(infoData);
setPriceInfo(priceData);
setLoading(false);
//loading 상태를 false로 만드는 것 잊지 말아라
})();
}, [coinId]);
//[] 안에 coinId를 넣어주면 더 좋다
const [info, setInfo] = useState<InfoData>();
const [priceInfo, setPriceInfo] = useState<PriceData>();
return (
<Container>
<Header>
<Title>{state?.name ? state.name : loading ? 'Loading...' : info?.name}</Title>
</Header>
{loading ? (
<Loader>Loading...</Loader>
) : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{info?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${info?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Open Source:</span>
<span>{info?.open_source ? 'Yes' : 'No'}</span>
</OverviewItem>
</Overview>
<Description>{info?.description}</Description>
<Overview>
<OverviewItem>
<span>Total Suply:</span>
<span>{priceInfo?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Supply:</span>
<span>{priceInfo?.max_supply}</span>
</OverviewItem>
</Overview>
<Tabs>
<Tab isActive={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActive={priceMatch !== null}>
<Link to={`/${coinId}/price`}>Price</Link>
</Tab>
</Tabs>
<Link to={`/${coinId}/chart`}>Chart</Link>
<Link to={`/${coinId}/price`}>Price</Link>
{/* 다양한 URL 로 Switch 하기 */}
<Switch>
<Route path={`/${coinId}/price`}>
<Price />
</Route>
<Route path={`/${coinId}/chart`}>
<Chart />
</Route>
</Switch>
</>
)}
</Container>
);
}
interface RouteState {
name: string;
}
interface RouteParams {
coinId: string;
}
interface InfoData {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
description: string;
message: string;
open_source: boolean;
started_at: string;
development_status: string;
hardware_wallet: boolean;
proof_type: string;
org_structure: string;
hash_algorithm: string;
first_data_at: string;
last_data_at: string;
}
interface PriceData {
id: string;
name: string;
symbol: string;
rank: number;
circulating_supply: number;
total_supply: number;
max_supply: number;
beta_value: number;
first_data_at: string;
last_updated: string;
quotes: {
USD: {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_6h: number;
percent_change_7d: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_from_price_ath: number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
};
};
}
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const Header = styled.header`
height: 20vh;
display: flex;
justify-content: center;
align-items: center;
`;
const Title = styled.h1`
font-size: 50px;
color: ${(props) => props.theme.accentColor};
`;
const Loader = styled.span`
display: block;
text-align: center;
`;
const Overview = styled.div`
display: flex;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 10px;
`;
const OverviewItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
span:first-child {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
margin-bottom: 5px;
}
`;
const Description = styled.p`
margin: 20px 0px;
`;
const Tabs = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
margin: 25px 0px;
gap: 10px;
`;
const Tab = styled.span<{ isActive: boolean }>`
//props 받기
text-align: center;
text-transform: uppercase;
font-size: 12px;
font-weight: 400;
background-color: rgba(0, 0, 0, 0.5);
padding: 7px 0px;
border-radius: 10px;
color: ${(props) => (props.isActive ? props.theme.accentColor : props.theme.textColor)};
a {
display: block;
}
`;
export default Coin;