【React】【Typescript】fetchAPIで天気情報を取得する

自分用のメモ

環境の準備

実行環境

  • VS code:v1.62.3
  • Node.js:v14.17.3
  • React:v17.0.2

プロジェクトの作成(Typescriptで作成)

React Create Appで作成します。

天気情報の公開APIを取得する

https://www.weatherapi.com/

React Create Appで作成されたソースの変更

fetchAPIの実装

  • 24行目にAPIキーを設定します。
  • 40行目で0.5秒待つようにして、Loadingアニメーションを見せるようにしています。
import { useState } from "react"
import './App.css'
import Title from './components/Title';
import Form from './components/Form';
import Results from './components/Results';
import Loading from "./components/Loading";
import { ResultsStateType } from "./components/ResultStateType";

const App = () => {
  const [loading, setLoading] = useState<boolean>(false)
  const [city, setCity] = useState<String>('')
  const [results, setResults] = useState<ResultsStateType>({
    country: '',
    cityName: '',
    temperature: '',
    conditionText: '',
    icon: ''
  })
  const getWeather = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault() //submitをキャンセル
    setLoading(true)

    // テンプレートリテラル「${}」を使う場合は、引用符をクォーテーションではなくバッククォートで囲む
    const key = '自身で作成したAPIキー'
    const url = `https://api.weatherapi.com/v1/current.json?key=${key}&q=${city}&aqi=no`
    setTimeout(() => (
      fetch(url)
        .then(res => res.json())
        .then(data => {
          setResults({
            country: data.location.country,
            cityName: data.location.name,
            temperature: data.current.temp_c,
            conditionText: data.current.condition.text,
            icon: data.current.condition.icon
          })
          setLoading(false)
        })
        .catch(err => alert('エラーが発生しました。'))
    ), 500)
  }
  return (
    <div className="wrapper">
      <div className="container">
        <Title />
        <Form setCity={setCity} getWeather={getWeather} />
        {loading ? <Loading /> : <Results results={results} />}
      </div>
    </div>
  )
}

export default App

CSSの実装(無くても動作します)

@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap');

html {
    font-size: 62.5%;
}

body {
    font-family: 'Raleway','Noto Sans JP', sans-serif;
    font-weight: 400;
    font-size: 1.6rem;
    font-display: swap;
    color: #333;
    margin: 0;
    padding: 0;
}

/* App.js */
.wrapper {
   width: 100vw;
   height: 100vh;
   display: flex;
   justify-content: center;
   align-items: center; 
   /* background: url("./background-image.jpg") center center no-repeat; */
   background-size: cover;
}

.container {
    width: 50vw;
    background-color: aqua;
    text-align: center;
    padding: 40px 20px;
    border-radius: 15px;
    backdrop-filter: blur(20px);
    box-shadow: 4px 4px 13px 5px rgba(0,0,0,0.25);
}
@media only screen and (max-width: 700px) {
    .container{
        width: 80vw;
    }
}

/* Title.js */
h1 {
    font-weight: 400;
    margin: 0px 0 50px;
}

/* Form.js */
input[type="text"] {
    background-color: transparent;
    border: 0;
    border-bottom: solid 1px #f15186;
    width: 40%;
    padding-bottom: 4px;
    /* color: #fff !important; */
    font-weight: lighter;
    letter-spacing: 2px;
    margin-bottom: 30px;
    margin-right: 20px;
    font-size: 20px;
}

button {
    width: 40%;
    border: 0;
    padding: 8px 20px;
    margin: 0 2px;
    border-radius: 2px;
    letter-spacing: 1px;
    font-size: 1.5rem;
    cursor: pointer;
    background-color: #f15186;
    color: #fff;
}

button:hover {
    opacity: 0.9
}

*:focus {
    outline: none;
}

/* Results.js */
.results-city {
    font-size: 4rem;
}

.results-country {
    font-size: 2rem;
}

.results-temp {
    font-size: 6rem;
    margin: 10px 0;
    color: #f15186;
}

.results-temp > span {
    font-size: 3rem;
    color: #333;
}

.results-condition {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2.5rem;
}

/* Loading.js */
.loading {
    border: 4px solid #f15186;
    border-top: 4px solid #ffffff;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    margin: auto;
    animation: spin 2s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

components配下のソース

Titleの実装(HTMLのみ)

const Title = () => <h1>ワールドウェザー</h1>

export default Title

Formの実装(都市を入力するためのテキストボックスと実行ボタン)

  • 7行目のpropsを分割代入しています。
import React from "react"

type FormPropsType = {
  setCity: React.Dispatch<React.SetStateAction<String>>
  getWeather: (e: React.FormEvent<HTMLFormElement>) => void
}
const Form = ({setCity, getWeather}: FormPropsType) => {
  return (
    <form onSubmit={getWeather}>
      <input type='text' name='city' placeholder='都市名' onChange={e => setCity(e.target.value)} />
      <button type='submit'>Get Weather</button>
    </form>
  )
}

export default Form

Resultsの実装(APIの取得結果を表示)

  • 7行目のように、propsの数が多い場合は一旦変数化したほうが見やすい
import { ResultsStateType } from "./ResultStateType"

type ResultsPropsType = {
  results: ResultsStateType
}
const Results = (props: ResultsPropsType) => {
  const {country, cityName, temperature, conditionText, icon} = props.results
  return (
    <>
      {cityName && 
        <div>{cityName}</div>}
      {country && 
        <div>{country}</div>}
      {temperature && 
        <div>{temperature}<span>℃</span></div>}
      {conditionText && 
        <div className='result-condition'>
          <img src={icon} alt='icon' />
          <span>{conditionText}</span>
        </div>}
    </>
  )
}

export default Results

Loadingの実装(実装自体はCSS)

const Loading = () => <div className='loading'></div>

export default Loading

Typeの実装(AppとResultsで共通化)

export type ResultsStateType = {
  country: string
  cityName: string
  temperature: string
  conditionText: string
  icon: string
}

あとがき

特になし

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です