環境の準備
Create React Appで新しいReactアプリを作成する
npx create-react-app <プロジェクト名> --template typescript
環境構築
PrettierとESLintを基本からまとめてみた【3】【ReactのアプリにESLintとPrettierを導入】
$ npm i redux react-redux redux-thunk redux-devtools-extension @types/react-redux bulma
使用しないファイルを削除
App.test.tesx
index.css
logo.svg
servicsWorker.ts
setupTest.ts
reportWebVitals.ts
.env ファイルを作成
//.env
REACT_APP_API_KEY=your_api_key_here
コンポーネント・ファイル構成
src
├── assets
└── components
├── About.jsx
├── Contact.jsx
├── Home.jsx
├── Nabvar.jsx
├── Skills.jsx
└── Work.jsx
├── App.css
├── App.tsx
├── index.tsx
└── react-app-env.d.ts
├── .env
├── postcss.config.js
├── tailwind.config.js
Weather API
公式サイト:Weather API
src / store /types.ts を作成
src/store/types.ts
export const GET_WEATHER = 'GET_WEATHER';
export const SET_LOADING = 'SET_LOADING';
export const SET_ERROR = 'SET_ERROR';
export const SET_ALERT = 'SET_ALERT';
export interface Weather {
description: string;
icon: string;
id: number;
main: string;
}
export interface WeatherData {
base: string;
clouds: {
all: number;
};
cod: number;
coord: {
lon: number;
lat: number;
};
dt: number;
id: number;
main: {
feels_like: number;
humidity: number;
pressure: number;
temp: number;
temp_max: number;
temp_min: number;
};
name: string;
sys: {
country: string;
id: number;
sunrise: number;
sunset: number;
type: number;
};
timezone: number;
visibility: number;
weather: Weather[];
wind: {
speed: number;
deg: number;
};
}
export interface WeatherError {
cod: string;
message: string;
}
export interface WeatherState {
data: WeatherData | null;
loading: boolean;
error: string;
}
interface GetWeatherAction {
type: typeof GET_WEATHER;
payload: WeatherData;
}
interface SetLoadingAction {
type: typeof SET_LOADING;
}
interface SetErrorAction {
type: typeof SET_ERROR;
payload: string;
}
export type WeatherAction = GetWeatherAction | SetLoadingAction | SetErrorAction;
export interface AlertAction {
type: typeof SET_ALERT;
payload: string;
}
export interface AlertState {
message: string;
}
src / store / reducers /weatherReducer.ts を作成
src/store/reducers/weatherReducer.ts
import { WeatherState, WeatherAction, GET_WEATHER, SET_LOADING, SET_ERROR } from "../types";
const initialState: WeatherState = {
data: null,
loading: false,
error: ''
}
export default (state = initialState, action: WeatherAction): WeatherState => {
switch(action.type) {
case GET_WEATHER:
return {
data: action.payload,
loading: false,
error: ''
}
case SET_LOADING:
return {
...state,
loading: true
}
case SET_ERROR:
return {
...state,
error: action.payload,
loading: false
}
default:
return state;
}
}
src / store / reducers /alertReducer.ts を作成
src/store/reducers/alertReducer.ts
import { AlertState, AlertAction, SET_ALERT } from "../types";
const initialState: AlertState = {
message: ''
}
export default (state = initialState, action: AlertAction): AlertState => {
switch(action.type) {
case SET_ALERT:
return {
message: action.payload
}
default:
return state;
}
}
src / store / index.ts を作成
src/store/index.ts
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';
import weatherReducer from './reducers/weatherReducer';
import alertReducer from './reducers/alertReducer';
const rootReducer = combineReducers({
weather: weatherReducer,
alert: alertReducer
});
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunk))
);
export type RootState = ReturnType<typeof rootReducer>;
export default store;
src / store / actions / weatherActions.ts を作成
公式サイト:OpenWeather
① By city ID の API callを下記のように編集する。
export const getWeather = (city: string): ThunkAction<void, RootState, null, WeatherAction> => {
return async dispatch => {
try {
const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?id={city id}&appid={API key}`);
store/actions/weatherActions.ts
import { ThunkAction } from 'redux-thunk';
import { RootState } from '..';
import { WeatherAction, WeatherData, WeatherError, GET_WEATHER, SET_LOADING, SET_ERROR } from '../types';
export const getWeather = (city: string): ThunkAction<void, RootState, null, WeatherAction> => {
return async dispatch => {
try {
const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.REACT_APP_API_KEY}`);
if(!res.ok) {
const resData: WeatherError = await res.json();
throw new Error(resData.message);
}
const resData: WeatherData = await res.json();
dispatch({
type: GET_WEATHER,
payload: resData
});
}catch(err) {
dispatch({
type: SET_ERROR,
payload: '',
//payload: err.message
});
}
}
}
export const setLoading = (): WeatherAction => {
return {
type: SET_LOADING
}
}
export const setError = (): WeatherAction => {
return {
type: SET_ERROR,
payload: ''
}
}
src / store / actions / alertActions.ts を作成
store/actions/alertActions.ts
import { SET_ALERT, AlertAction } from '../types';
export const setAlert = (message: string): AlertAction => {
return {
type: SET_ALERT,
payload: message
}
}
src/index.tsx を作成
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import store from './store';
import '../node_modules/bulma/css/bulma.min.css';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
src / components / Search.tsx を作成
src/components/Search.tsx
import React, { FC, useState, FormEvent } from 'react';
import { useDispatch } from 'react-redux';
import { setAlert } from '../store/actions/alertActions';
import { getWeather, setLoading } from '../store/actions/weatherActions';
interface SearchProps {
title: string;
}
const Search: FC<SearchProps> = ({ title }) => {
const dispatch = useDispatch();
const [city, setCity] = useState('');
const changeHandler = (e: FormEvent<HTMLInputElement>) => {
setCity(e.currentTarget.value);
}
const submitHandler = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if(city.trim() === '') {
return dispatch(setAlert('City is required!'));
}
dispatch(setLoading());
dispatch(getWeather(city));
setCity('');
}
return(
<div className="hero is-light has-text-centered">
<div className="hero-body">
<div className="container">
<h1 className="title">{title}</h1>
<form className="py-5" onSubmit={submitHandler}>
<input
type="text"
className="input has-text-centered mb-2"
placeholder="Enter city name"
style={{maxWidth: 300}}
value={city}
onChange={changeHandler}
/>
<button className="button is-primary is-fullwidth" style={{maxWidth: 300, margin: '0 auto'}}>Search</button>
</form>
</div>
</div>
</div>
);
}
export default Search;
src / components / Alert.tsx を作成
src/components/Alert.tsx
import React, { FC } from 'react';
interface AlertProps {
message: string;
onClose: () => void
}
const Alert: FC<AlertProps> = ({ message, onClose }) => {
return(
<div className="modal is-active has-text-centered">
<div className="modal-background" onClick={onClose}></div>
<div className="modal-card">
<header className="modal-card-head has-background-danger">
<p className="modal-card-title has-text-white">{message}</p>
</header>
<footer className="modal-card-foot" style={{justifyContent: 'center'}}>
<button className="button" onClick={onClose}>Close</button>
</footer>
</div>
</div>
);
}
export default Alert;
src / components / Weather.tsx を作成
src/components/Weather.tsx
import React, { FC } from 'react';
import { WeatherData } from '../store/types';
interface WeatherProps {
data: WeatherData;
}
const Weather: FC<WeatherProps> = ({ data }) => {
const fahrenheit = (data.main.temp * 1.8 - 459.67).toFixed(2);
const celsius = (data.main.temp - 273.15).toFixed(2);
return(
<section className="section">
<div className="container">
<h1 className="title has-text-centered" style={{marginBottom: 50}}>{data.name} - {data.sys.country}</h1>
<div className="level" style={{alignItems: 'flex-start'}}>
<div className="level-item has-text-centered">
<div>
<p className="heading">{data.weather[0].description}</p>
<p className="title"><img src={`http://openweathermap.org/img/wn/${data.weather[0].icon}.png`} alt=""/></p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">temp</p>
<div className="title">
<p className="mb-2">{data.main.temp}K</p>
<p className="mb-2">{fahrenheit}<sup>℉</sup></p>
<p>{celsius}<sup>℃</sup></p>
</div>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">humidity</p>
<p className="title">{data.main.humidity}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">pressure</p>
<p className="title">{data.main.pressure}</p>
</div>
</div>
<div className="level-item has-text-centered">
<div>
<p className="heading">wind</p>
<p className="title">{data.wind.speed} m/s</p>
</div>
</div>
</div>
</div>
</section>
);
}
export default Weather;
src / App.css を作成
src/App.css
.level .level-item {
margin-bottom: 50px !important;
}
src / App.tsx を作成
src/App.tsx
import React, { FC } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import './App.css';
import { RootState } from './store';
import Search from './components/Search';
import Alert from './components/Alert';
import Weather from './components/Weather';
import { setAlert } from './store/actions/alertActions';
import { setError } from './store/actions/weatherActions';
const App: FC = () => {
const dispatch = useDispatch();
const weatherData = useSelector((state: RootState) => state.weather.data);
const loading = useSelector((state: RootState) => state.weather.loading);
const error = useSelector((state: RootState) => state.weather.error);
const alertMsg = useSelector((state: RootState) => state.alert.message);
return (
<div className="has-text-centered">
<Search title="Enter city name and press search button" />
{loading ? <h2 className="is-size-3 py-2">Loading...</h2> : weatherData && <Weather data={weatherData} />}
{alertMsg && <Alert message={alertMsg} onClose={() => dispatch(setAlert(''))} />}
{error && <Alert message={error} onClose={() => dispatch(setError())} />}
</div>
);
}
export default App;