1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React ( Redux ToolKit + TypeScript )の構成でアプリを作成しました【Weather Application】

Last updated at Posted at 2022-05-23

学習するスキル

  • TypeScript+Reactの構成で、Applicationを開発するスキル
  • 外部APIを利用するスキル(openWeather, googleMap)
  • ドキュメントを読むスキル
  • バックエンドと連携するスキル

機能及び特徴

  • 任意の都市や場所の天気予報
  • 7日間の延長予報
  • GeolocationAPIを利用したユーザーの位置情報の取得
  • 摂氏から華氏への変換、およびその逆をワンクリックで実行可能

環境の準備

①ターミナルでreactアプリケーションを作成する

$ mkdir ディレクトリ名
$ npx create-react-app . --template redux-typescript --use-npm
$ npm start

② ESLintとPrettier を導入する

ESLintとPrettierとを基本からまとめてみた【React+TypeScriptのアプリにESLintとPrettierを導入】

③必要なパッケージをインストールする

$ npm install axios
$ npm install chart.js@2.9.3
$ npm install react-chartjs-2@2.9.0 --legacy-peer-deps
$ npm install @react-google-maps/api use-places-autocomplete
$ npm install @react-google-maps/api
$ npm install react-geocode
$ npm install npm-sass

コンポーネント・ファイル構成

 src
  ├─ app
       ├── hooks.ts
       └── store.ts
  ├─ components
            ├── Chart
                 └── Chart.tsx
            ├── DashBoardWeather
                 ├── DashBoardWeather.module.scss
           └── DashBoardWeather.tsx
            ├── initialweather
                 ├── initialweather.module.scss
           └── initialweather.tsx
             └── SearchArea
                 ├── SearchArea.module.scss
            └── SearchArea.tsx            
    └── features
             └── api
                  └── locationSlice.tsx
  ├── App.module.scss
  ├── App.tsx
  ├── index.module.scss
  └── index.tsx
└──.env
src/app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
src/app/store.ts
import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit';
import locationReducer from '../features/api/locationSlice';

export const store = configureStore({
  reducer: {
    location: locationReducer,
  },
});

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;
src/components/Chart/Chart.tsx
import React, { useEffect, useState } from 'react';
import { Line } from 'react-chartjs-2';

const Chart = (props: any) => {
  const [nowtimes, setNowTimes] = useState<number[]>([]);
  const [temphourly, setTempHourly] = useState<number[]>([]);

  useEffect(() => {
    const weatherData = props.data;
    const honlyDatas = weatherData.hourly;
    console.log(honlyDatas);
    if (honlyDatas) {
      const timeList = [];
      const temperature = [];
      for (let i = 0; i < 7; i++) {
        timeList.push(new Date(honlyDatas[i].dt * 1000).getHours());
      }
      setNowTimes(timeList);

      for (let i = 0; i < 7; i++) {
        temperature.push(Math.fround(honlyDatas[i].temp - 273.15));
      }
      setTempHourly(temperature);
    }
  }, [props.data]);
  console.log(nowtimes);
  console.log(temphourly);

  interface Datas {
    labels: number[];
    datasets: [
      {
        label: string;
        backgroundColor: string;
        borderColor: string;
        pointBorderWidth: number;
        data: number[];
      }
    ];
  }

  const data: Datas = {
    labels: [...nowtimes],
    datasets: [
      {
        label: '1時間ごとの気温',
        backgroundColor: '#e6e6e6',
        borderColor: '#2e2d2d',
        pointBorderWidth: 2,
        data: [...temphourly],
      },
    ],
  };

  const graphOption = {
    scales: {
      xAxes: [
        {
          scaleLabel: {
            display: true,
            labelString: '時刻',
          },
        },
      ],
      yAxes: [
        {
          scaleLabel: {
            display: true,
            labelString: '平均気温(℃)',
            ticks: {
              stepSize: 1,
              fontColor: 'blue',
              fontSize: 14,
            },
          },
        },
      ],
    },
  };
  return (
    <div>
      <Line
        type={Line}
        height={50}
        width={50}
        data={data}
        options={graphOption}
      />
    </div>
  );
};

export default React.memo(Chart);
src/components/DashBoardWeather/DashBoardWeather.module.css
.icon_temperature {
  display: flex;
  text-align: center;
  align-items: center;
  justify-content: center;
}

.title {
  margin-right: 16px;
  font-weight: bold;
}
.Details_container {
  margin-top: 6px;
  padding: 0 21px;
}
.today_Details {
  display: flex;
  text-align: center;
  align-items: center;
  justify-content: center;
}

.today_icon {
  height: 50px;
  width: 50px;
}

.today_title {
  font-weight: bold;
  color: rgb(83, 84, 84);
  font-size: 36px;
}

.today_Details_info {
  margin-right: 16px;
}

.week_weather_container {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}

.each_weather {
  width: 110px;
  text-align: center;
  height: 110px;
}

.data_title {
  margin-bottom: 0;
  text-align: center;
}
.temperature_title {
  margin-top: 0;
}

.title_label {
  margin-top: 45px;
  margin-bottom: 0;
  font-weight: bold;
  font-size: 20px;
  color: rgb(245, 154, 93);
}
src/components/DashBoardWeather/DashBoardWeather.tsx
import React from 'react';
import styles from './DashBoardWeather.module.scss';
import InitialWeather from '../initialweather/InitialWeather';

const DisplayWeather = (props: any) => {
  const weatherData = props.data;
  const dailyDatas = weatherData.daily;
  const todayDatas = weatherData.current;
  console.log(props.data);
  console.log(dailyDatas);

  return (
    <>
      {dailyDatas ? (
        <div className={styles.displayweather}>
          <div className={styles.today_weather}>
            <div className={styles.icon_temperature}>
              <img
                className={styles.today_icon}
                src={
                  'http://openweathermap.org/img/wn/' +
                  `${todayDatas.weather[0].icon}` +
                  '.png'
                }
                alt=''
              />
              <span className={styles.today_title}>
                {Math.floor(todayDatas.temp - 273.15)}°C
              </span>
            </div>
            <div className={styles.temperature_info}>
              <span className={styles.title}>
                (最高気温):
                {Math.floor(dailyDatas[0].temp.max - 273.15)}°C
              </span>
              <span className={styles.title}>
                (最低気温):
                {Math.floor(dailyDatas[0].temp.min - 273.15)}°C
              </span>
            </div>
            <div className={styles.Details_container}>
              <div className={styles.today_Details}>
                <div className={styles.today_Details_info}>
                  Wind speed(風速):{todayDatas.wind_speed}m/s
                </div>
                <div className={styles.today_Details_info}>
                  Barometric pressure(気圧):{todayDatas.pressure}hPa
                </div>
                <div className={styles.today_Details_info}>
                  Temperature(湿度):{todayDatas.humidity}%
                </div>
              </div>
              <p className={styles.title_label}>8日間の天気</p>
            </div>
          </div>

          <div className={styles.week_weather_container}>
            {dailyDatas.map((dailyData: any, index: number) => {
              return (
                <div className={styles.each_weather}>
                  <p className={styles.data_title}>
                    {new Date(dailyData.dt * 1000)
                      .toLocaleDateString('ja-JP')
                      .slice(5)}
                  </p>
                  <img
                    src={
                      dailyData.weather
                        ? 'http://openweathermap.org/img/wn/' +
                          `${dailyData.weather[0].icon}` +
                          '.png'
                        : 'http://openweathermap.org/img/wn/01d.png'
                    }
                    alt=''
                    className={styles.icon}
                  />
                  <p className={styles.temperature_title}>
                    {Math.floor(dailyData.temp.max - 273.15)}/
                    {Math.floor(dailyData.temp.min - 273.15)}°C
                  </p>
                </div>
              );
            })}
          </div>
        </div>
      ) : (
        <div className={styles.displayweather}>
          <div className={styles.today_weather}>
            <div className={styles.icon_temperature}>
              <img
                className={styles.today_icon}
                src={'http://openweathermap.org/img/wn/01d.png'}
                alt=''
              />
              <span className={styles.today_title}>--°C</span>
            </div>
            <div className={styles.temperature_info}>
              <span className={styles.title}>最高気温 : --°C</span>
              <span className={styles.title}>最低気温 : --°C</span>
            </div>
            <div className={styles.Details_container}>
              <div className={styles.today_Details}>
                <div className={styles.today_Details_info}>風速 : --m/s</div>
                <div className={styles.today_Details_info}>気圧 : --hPa</div>
                <div className={styles.today_Details_info}>湿度 : --%</div>
              </div>
              <p className={styles.title_label}>8日間の天気</p>
            </div>
          </div>

          <InitialWeather />
        </div>
      )}
    </>
  );
};

export default React.memo(DisplayWeather);
src/components/initialweather/initialweather.module.css
.week_weather_container {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}

.each_weather {
  width: 110px;
  text-align: center;
  height: 110px;
}

.data_title {
  margin-bottom: 0;
  text-align: center;
}

.today_icon {
  height: 50px;
  width: 50px;
}
.temperature_title {
  margin-top: 0;
}
src/components/initialweather/initialweather.tsx
import React from 'react';
import styles from './InitialWeather.module.scss';

const InitialWeather = () => {
  const d = new Date();
  const addDate = 1;
  const max = 8;
  const func = () => {
    let a = '';
    for (let i = 0; i <= max; i++) {
      d.setDate(d.getDate() + addDate);
      return (a = d.getMonth() + 1 + '/' + d.getDate());
    }
  };
  return (
    <>
      <div className={styles.week_weather_container}>
        <div className={styles.each_weather}>
          <p className={styles.data_title}>{func()}</p>
          <img
            src={'http://openweathermap.org/img/wn/01d.png'}
            alt=''
            className={styles.icon}
          />
          <p className={styles.temperature_title}>--/-- °C</p>
        </div>

        <div className={styles.each_weather}>
          <p className={styles.data_title}>{func()}</p>
          <img
            src={'http://openweathermap.org/img/wn/01d.png'}
            alt=''
            className={styles.icon}
          />
          <p className={styles.temperature_title}>--/-- °C</p>
        </div>

        <div className={styles.each_weather}>
          <p className={styles.data_title}>{func()}</p>
          <img
            src={'http://openweathermap.org/img/wn/01d.png'}
            alt=''
            className={styles.icon}
          />
          <p className={styles.temperature_title}>--/-- °C</p>
        </div>

        <div className={styles.each_weather}>
          <p className={styles.data_title}>{func()}</p>
          <img
            src={'http://openweathermap.org/img/wn/01d.png'}
            alt=''
            className={styles.icon}
          />
          <p className={styles.temperature_title}>--/-- °C</p>
        </div>

        <div className={styles.each_weather}>
          <p className={styles.data_title}>{func()}</p>
          <img
            src={'http://openweathermap.org/img/wn/01d.png'}
            alt=''
            className={styles.icon}
          />
          <p className={styles.temperature_title}>--/-- °C</p>
        </div>

        <div className={styles.each_weather}>
          <p className={styles.data_title}>{func()}</p>
          <img
            src={'http://openweathermap.org/img/wn/01d.png'}
            alt=''
            className={styles.icon}
          />
          <p className={styles.temperature_title}>--/-- °C</p>
        </div>

        <div className={styles.each_weather}>
          <p className={styles.data_title}>{func()}</p>
          <img
            src={'http://openweathermap.org/img/wn/01d.png'}
            alt=''
            className={styles.icon}
          />
          <p className={styles.temperature_title}>--/-- °C</p>
        </div>

        <div className={styles.each_weather}>
          <p className={styles.data_title}>{func()}</p>
          <img
            src={'http://openweathermap.org/img/wn/01d.png'}
            alt=''
            className={styles.icon}
          />
          <p className={styles.temperature_title}>--/-- °C</p>
        </div>
      </div>
    </>
  );
};

export default InitialWeather;
src/components/SearchArea/SearchArea.module.css
.form_container {
  text-align: center;
  margin-top: 40px;
}

.valuefield {
  width: 40%;
  padding: 10px;
  border: 1px solid rgb(208, 205, 205);
  border-radius: 1px;
  margin-bottom: 20px;
}

.submit_btn {
  margin-left: 10px;
  padding: 10px 20px;
  width: 15%;
  background-color: rgb(245, 154, 93);
  border: none;
  color: #fff;
  border-radius: 1px;
}

.submit_btn:hover {
  opacity: 0.7;
}

.weather_area {
  margin-top: 10px;
  display: flex;
}

.display_left {
  width: 48%;
  text-align: center;
  height: 100%;
}

.display_right {
  width: 48%;
  height: 100%;
}

.today_time_title {
  color: rgb(245, 154, 93);
  font-weight: bold;
  font-size: 20px;
  margin-bottom: 0;
}

.area_title {
  font-weight: bold;
  font-size: 28px;
  margin-top: 0;
  margin-bottom: 0px;
}

.label_title {
  margin: 0;
  text-align: left;
  position: relative;
  left: 230px;
  font-weight: bold;
}

.label {
  margin-bottom: 0;
  font-weight: bold;
  font-size: 20px;
  color: rgb(245, 154, 93);
  text-align: center;
}
src/components/SearchArea/SearchArea.tsx
import React, { useState, useEffect } from 'react';
import styles from './SearchArea.module.scss';
import { useDispatch, useSelector } from 'react-redux';
import { setLocationCity, selectCity } from '../../features/api/locationSlice';
import axios from 'axios';
import Geocode from 'react-geocode';
import { GoogleMap, LoadScript } from '@react-google-maps/api';
import DisplayWeather from '../DashBoardWeather/DashBoardWeather';
import Chart from '../Chart/Chart';

const SearchArea = () => {
  const dispatch = useDispatch();
  const cityLocation = useSelector(selectCity);
  const APIKEY: any = process.env.REACT_APP_GOOGLE_API_KEY;
  const APIKEY_GEOCODE: any = process.env.REACT_APP_WEATHER_API_KEY;
  const [city, setCity] = useState('');
  const [latstate, setLatstate] = useState(35.68944);
  const [lngstate, setLngstate] = useState(139.9167);
  const [weather, setWeather] = useState([]);
  const today = new Date();
  const month = today.getMonth() + 1;
  const day = today.getDate();

  const [center, setCenter] = useState({ lat: 35.68944, lng: 139.9167 });
  const [currentPosition, setCurrentPosition] = useState({});

  const firstlocation = async () => {
    await axios
      .get(
        `https://api.openweathermap.org/data/2.5/onecall?lat=${center.lat}&lon=${center.lng}&lang=ja&appid=${APIKEY_GEOCODE}`
      )
      .then((response) => {
        const data: any = response.data;
        setWeather(data);
        console.log(weather);
        console.log('status:', response.status);
      })
      .catch((err) => {
        console.log('err:', err);
      });
  };

  const success = (data: any) => {
    const currentPosition = {
      lat: data.coords.latitude,
      lng: data.coords.longitude,
    };
    setCurrentPosition(currentPosition);
    setCenter(currentPosition);
    firstlocation();
  };

  const error = (data: any) => {
    const currentPosition = {
      lat: 34.673542,
      lng: 135.433338,
    };
    setCurrentPosition(currentPosition);
    setCenter(currentPosition);
  };
  useEffect(() => {
    navigator.geolocation.getCurrentPosition(success, error);
  }, []);

  const weatherData = async (e: any) => {
    dispatch(setLocationCity(city));
    Geocode.setApiKey(APIKEY);
    Geocode.fromAddress(city).then(
      async (response) => {
        const { lat, lng } = response.results[0].geometry.location;
        setLatstate(lat);
        setLngstate(lng);
        setCenter({ lat, lng });
        await axios
          .get(
            `https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lng}&lang=ja&appid=${APIKEY_GEOCODE}`
          )

          .then((response) => {
            const data: any = response.data;
            setWeather(data);
            console.log(weather);
            console.log('status:', response.status);
          })
          .catch((err) => {
            console.log('err:', err);
          });
      },
      (error) => {
        alert('error');
      }
    );

    e.preventDefault();
    if (city === '') {
      alert('値を追加してください');
    } else {
      console.log(city);

      setCity('');
    }
  };

  const containerStyle = {
    width: '300px',
    height: '200px',
    margin: '0 auto',
  };

  return (
    <>
      <div className={styles.container}>
        <div className={styles.form_container}>
          {/* <p className={styles.label_title}>都市名</p> */}
          <form action=''>
            <input
              type='text'
              name='city'
              placeholder='Enter city name and press search button'
              value={city}
              onChange={(e) => setCity(e.target.value)}
              className={styles.valuefield}
            />
            <button
              className={styles.submit_btn}
              onClick={(e) => weatherData(e)}
            >
              Search
            </button>
          </form>
        </div>

        <div className={styles.map_container}>
          <LoadScript googleMapsApiKey={APIKEY}>
            <GoogleMap
              mapContainerStyle={containerStyle}
              center={center}
              zoom={8}
            ></GoogleMap>
          </LoadScript>
        </div>
      </div>
      <div className={styles.weather_area}>
        <div className={styles.display_left}>
          <p className={styles.today_time_title}>
            <span>現在時刻</span>
            {month + '' + day + ''}
          </p>
          <p className={styles.area_title}>{cityLocation}</p>

          {weather && (
            <>
              <DisplayWeather data={weather} />
            </>
          )}
        </div>
        <div className={styles.display_right}>
          <p className={styles.label}>1時間ごとの天気</p>
          <Chart data={weather} />
        </div>
      </div>
    </>
  );
};

export default SearchArea;
src/features/api/locationSlice.tsx
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RootState, AppThunk } from '../../app/store';

export interface LocationState {
  city: string;
  lat: number;
  lng: number;
}
const initialState: LocationState = {
  city: '現在地',
  lat: 35.6761919,
  lng: 139.7690174,
};

export const locationSlice = createSlice({
  name: 'location',
  initialState,
  // The `reducers` field lets us define reducers and generate associated actions
  reducers: {
    setLocationCity: (state, action) => {
      state.city = action.payload;
    },

    latLocation: (state, action) => {
      state.lat = action.payload;
    },
    lngLocation: (state, action) => {
      state.lng = action.payload;
    },
  },
});

export const { setLocationCity, latLocation, lngLocation } =
  locationSlice.actions;

export const selectCity = (state: RootState): string => state.location.city;
export const selectLat = (state: RootState): number => state.location.lat;
export const selectLng = (state: RootState): number => state.location.lng;

export default locationSlice.reducer;
src/App.module.scss
.App {
  margin: 0 auto;
  width: 100%;
  max-width: 980px;
}
src/App.tsx
import React from 'react';
import SearchArea from './components/SearchArea/SearchArea';
import styles from './App.module.scss';

function App() {
  return (
    <div className={styles.App}>
      <SearchArea />
    </div>
  );
}
export default App;
src/index.module.scss
body {
  margin: 0;
  padding: 0px;
  height: 100%;
  width: 100%;
}
src/index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './index.module.scss';

const container = document.getElementById('root')!;
const root = createRoot(container);

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
.env
REACT_APP_GOOGLE_API_KEY=  your API
REACT_APP_WEATHER_API_KEY=  your API
.gitignore
//追加する
.env

参考サイト

TypeScript + Reactを基本からまとめてみた【3】【Setting up Google Maps】
Weather App with React & TypeScript
React (React Hooks + Redux ToolKit + TypeScript )の構成でアプリを作成しました【Covid19dashboard】
React (React Hooks + Redux ToolKit + TypeScript )の構成でアプリを作成しました【Covid19dashboard】

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?