React&TypeScriptでのreact-redux、redux-thunkお勉強メモ
フロントエンドのお勉強をしています。
とりあえず React をやってみてるのですが react-redux、redux-thunk がめっちゃ難しくて全然理解できないということで簡単なコードを書いて実行しながら勉強したメモです。
ハンズオンぽく書いてます。
結果だけ興味のある方は コチラ からどうぞ。
はじめに
環境
- OS : macOS Big Sur
- エディタ : VSCODE
- 言語 : React & TypeScript
- ブラウザ : Chrome
キーワード
以下の内容を雰囲気レベルで使っています。
- React Router( react-router-dom )
- Redux( redux, react-redux )
- Thunk( redux-thunk )
- Axios( axios )
- Redux DevTools( redux-devtools-extension )
細かい説明はWebにたくさんあるので検索して調べることにして、とにかく動くものを作っていきます。
何をつくるか
画面のボタンをクリックしたらHTTPリクエスト投げて結果を画面に表示するアプリを作ります。
Reactだけでつくります。
1. プロジェクト作成
npx create-react-appでプロジェクト作成します、react-catという TypeScript のプロジェクトを作ります。
npx create-react-app react-cat --template typescript
こんなプロジェクトができます。
react-cat
├── node_modules
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ └── setupTests.ts
├── tsconfig.json
└── yarn.lock
余計なものは消す
学習用のサンプルプログラムなので余計なものをなくしてシンプルな方がいいですね、ということで今回のサンプルに必要なさそうなものは極力削除します。
デフォルトのApp.tsx
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="<https://reactjs.org>"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
修正後のApp.tsx
const App = () => {
return (<p>Hello World</p>)
}
export default App;
デフォルトのindex.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// 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();
修正後のindex.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
いらないファイルを消す
Xのファイルはなくてもいいので削除します
react-cat
├── node_modules
├── README.md
├── package.json
├── public
├── src
│ ├── App.css ------------- X
│ ├── App.test.tsx -------- X
│ ├── App.tsx
│ ├── index.css ----------- X
│ ├── index.tsx
│ ├── logo.svg ------------ X
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts -- X
│ └── setupTests.ts ------- X
├── tsconfig.json
└── yarn.lock
動作確認
これで1回実行してみましょう、README.mdの説明に従って yarn start します。
yarn start
ブラウザが立ち上がって Hello World と出れば OK
2. ルーティングする
今回のサンプルは1ページだけなんですが 勉強のためにルーティングをやってみます。
いまは index.tsx → App.tsx(Hello World) という流れになっています、ここから App.tsxとは別にTopページを作ってそこを初期表示するようにしたいと思います。
React Router
react-router-dom をインストールします
yarn add react-router-dom
yarn add -D @types/react-router-dom
複数のページを持つアプリの場合 ルーティングが必要になります。React Router(react-router-dom) はルーティングの機能を提供するライブラリでApp.tsxをルーティングのためのコンポーネントにします。
App.tsxがルーティング専用になるのでそれとは別のTopページが必要になります。
src
の下に cat
フォルダを作成してその中に Top.tsx を新規作成します
Top.tsx
const Top = () => {
return (
<p>Cats rule the world.</p>
)
}
export default Top;
react-router-dom
の力を使って URLのパスが'/'の時にTopを表示するようにします
App.tsx
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Top from './cat/Top';
const App = () => {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Top} />
</Switch>
</BrowserRouter>
)
}
export default App;
動作確認
Cats rule the world. と表示されればOK
3. Redux入れてStoreを作成する
ここからが難解なところです。
アプリで共通で参照可能な Store という領域に データ(state) を格納します。
Redux
redux,react-reduxをインストールします
yarn add redux
yarn add react-redux
Redux は 例の 難解なモデル(名前がわからん)で state を Store なるもの に入れといて、ComponentからActionをdispatch して Reducerなるもの で Storeに入っているstate を更新するもの です。
全然わかりません。
とりあえずコードだけ書いてみます。
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux'
import getCatReducer from './cat/getCatReducer'
export const rootReducer = combineReducers({
getCatReducer,
});
const store = createStore(
rootReducer
);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
getCatReducer
が無い と怒られるので新規作成します
getCatReducer.ts
import {CatState} from './store';
import {Action, GET_CAT_REQUEST, GET_CAT_SUCCESS, GET_CAT_FAILURE} from './action';
const initalState:CatState = {
status: "init",
url: "",
lastUpdated: Date.now()
}
const getCatReducer = (state:CatState = initalState, action:Action):CatState => {
switch (action.type) {
case GET_CAT_REQUEST:
return ({
status: "Fetching",
url: "",
lastUpdated: Date.now()
})
case GET_CAT_SUCCESS:
return ({
status: "Success",
url: action.payload[0].url,
lastUpdated: Date.now()
})
case GET_CAT_FAILURE:
return ({
status: "Failure",
url: "",
lastUpdated: Date.now()
})
default:
return state
}
}
export default getCatReducer;
今度は store, action
が無いと怒られるのでまたまた作成します。
store.ts
export interface RootState {
getCatReducer: CatState
}
export interface CatState {
status: string,
url: string,
lastUpdated: Number
}
action.ts
export type Action = {
type: String,
payload: any
}
// Action リクエスト送信前
export const GET_CAT_REQUEST = 'GET_CAT_REQUEST'
// Action レスポンス受信
export const GET_CAT_SUCCESS = 'GET_CAT_SUCCESS'
// Action エラー
export const GET_CAT_FAILURE = 'GET_CAT_FAILURE'
動作確認
無駄に複雑なこれは一体何なんだとブツブツ言いながらコンパイルエラーは取れたので実行してみます。
何も変わりません、表示に関わるところはなにもしていないので当然です。
今回追加したのは (*) のところです。
- 立ち上げたら localhost:3000 を表示する
- まずは index.tsx を実行する
- combineReducers() で rootReducer を作成する (*)
- createStore() で Store を作成する (*)
-
Provider store={store}>
で 何かおまじないをして<App />
をレンダリングする (*)
- App.tsx の
<Route exact path="/" component={Top} />
で Top.tsx を表示する - Top.tsx を表示する → Cats rule the world.
store っていうのがReduxが管理する領域で データ(state)とstateにアクセスするメソッド(Reducer)を格納する箱みたいなものです。
この store がどうなっているのかデベロッパツールで見てみましょう。
index.tsx で store 実行した直後にブレイクポイント入れて store.getState()
すると store の中を見ることができます。
store.getState()
getCatReducer{
lastUpdated: xxxxx ,
status: "init" ,
url: ""
}
status="init"ってなってるんで getCatReducer()
が実行されてちゃんと initalState で初期化されているようです。
ここはReduxの重要なところなんでちゃんと理解しておく必要があるのですが今回は雰囲気理解なのでこれでヨシ。
4. WebAPIにリクエスト投げる
Top画面にボタンを配置してクリックしたらWebAPIを投げるようにしたいと思います。
やりたいことは
- WebAPIにリクエスト送信
- レスポンス受信
- 結果を Store に格納
です。
Thunk
まずは redux-thunk をインストールします
yarn add redux-thunk
WebAPIにリクエスト投げてレスポンスを受け取るところは非同期にして画面描画をフリーズさせないようにする必要があります。細かい仕組みはともかく Thunk(redux-thunk) を使わないといけないようです。
index.tsxにおまじないを追記します。
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import thunk from 'redux-thunk' // 追加
import { createStore, combineReducers, applyMiddleware } from 'redux'; // 追加applyMiddleware
import { Provider } from 'react-redux';
import getCatReducer from './cat/getCatReducer';
export const rootReducer = combineReducers({
getCatReducer,
});
const store = createStore(
rootReducer,
applyMiddleware(thunk) // 追加
);
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
リクエスト投げる先
投げる先はコチラ
投げるといいかんじの猫画像URLを返してくれます。
% curl '<https://api.thecatapi.com/v1/images/search>'
[
{
"breeds":[],
"id":"h9-Ir-v9B",
"url":"<https://cdn2.thecatapi.com/images/h9-Ir-v9B.jpg>",
"width":320,
"height":180
}
]
Axios
次に axios をインストールします
yarn add axios
axios は Promise ベースの HTTP クライアントで js なんかで非同期でHTTP通信する便利なものです。redux-thunkを使うと何も考えなくてもいいかんじでaxiosがreactで使えるようになります(雰囲気)。
ボタンクリックしたらリクエスト投げる
さっそく実装していきます
Top.tsx
import { useDispatch } from 'react-redux';
import getCatAction from './getCatAction';
const Top = () => {
const dispatch = useDispatch()
const buttonAlert = () => {
dispatch(getCatAction())
}
return (
<div>
<p>Cats rule the world.</p>
<button onClick={buttonAlert}>get cat</button>
</div>
)
}
export default Top;
getCatAction を追加します
getCatAction.ts
import axios from 'axios';
import {Action, GET_CAT_REQUEST, GET_CAT_SUCCESS, GET_CAT_FAILURE} from './action';
const getCatRequest = (): Action => {
return {
type: GET_CAT_REQUEST,
payload: null
}
}
const getCatSuccess = (json: Object): Action => {
return {
type: GET_CAT_SUCCESS,
payload: json
}
}
const getCatFailure = (error: Object): Action => {
return{
type: GET_CAT_FAILURE,
payload: error
}
}
const getCatAction = () => {
return (dispatch: any) => {
dispatch(getCatRequest())
return axios.get(`https://api.thecatapi.com/v1/images/search`)
.then(res =>
dispatch(getCatSuccess(res.data))
).catch(err =>
dispatch(getCatFailure(err))
)
}
}
export default getCatAction;
動作確認
実行するとTop画面に get cat
ボタンが追加されています。
get cat ボタンをクリックするとリクエスト投げてレスポンスが返ってきているようですが見た目何も変わらないので楽しくないですね。
内部的には
- get cat ボタンをクリック
- getCatAction() が実行される
-
dispatch(getCatRequest())
で GET_CAT_REQUEST によって Store に格納された state が更新される - axios.get()で リクエスト出す(ここが非同期に実行される)
- レスポンスが返ってくると then に入っていて
dispatch(getCatSuccess(res.data))
から GET_CAT_SUCCESS して Store に格納された state に GET した URL がセットされる
ってなっています。
Redux DevTools
この辺を楽しく確認できる Redux DevTools を導入します。
まずはコマンドでインストール
yarn add redux-devtools-extension
次に index.tsx を少し修正してcreateStore しているところで composeWithDevTools を追加します。
index.tsx
import { composeWithDevTools } from 'redux-devtools-extension';
...
const store = createStore(
rootReducer,
composeWithDevTools( // for Redux Dev Tools
applyMiddleware(thunk)
)
);
Chromeに 拡張機能 Redux DevTools をインストールします
Redux DevToolsを使うと 発行したAction とか State のデータを簡単に見ることができるのでちゃんと動いていることが確認できて楽しくなります。
5. Storeデータを画面表示する
ここまでで WebAPIからのレスポンスを受信して 猫画像のURLを Store に格納することができるようになりました。この情報を画面に表示するところを実装していきます。
Storeデータへのアクセスは connect() 使う方法と useSelector() 使う方法の二通りあるようですが、ここでは useSelector() を使います。
Top.tsx
import { useDispatch, useSelector } from 'react-redux';
import getCatAction from './getCatAction';
import { RootState } from './store';
const Top = () => {
const dispatch = useDispatch()
const buttonAlert = () => {
dispatch(getCatAction())
}
const state = useSelector((state:RootState) => state.getCatReducer)
return (
<div>
<p>Cats rule the world.</p>
<button onClick={buttonAlert}>get cat</button>
<br/>
<label>{state.status}</label>
<br/>
<label>{state.url}</label>
<br/>
<a target="_blank" rel="noopener noreferrer" href={state.url}>cat</a>
</div>
)
}
export default Top;
- useSelector() で Store から state(データ)を取り出しています
- TypeScriptなんで型を指定しないといけないということで store.ts に型情報を定義しているところが結構ポイントだったりします
-
state.status
とstate.url
をlabelにして表示します
ここでやっとヨッシャヨッシャ!な気持ちになりました
6. Componentにデータを引き渡す
いまは Top画面の リンク(aタグ) に ゲットした猫画像のURLを紐付けてクリックしたら表示、としています。
お勉強のために以下のようにしてみます。
- リンクを別のコンポーネントにする
- URLが無い(クリックしても意味がない)ときは非表示にする
自前のComponentを作って引数(Props)で渡す
Link
というComponent を作ってTop画面と分離します。
Linkは url を受け取ります。
url が空のときは非表示にします。
Link.tsxというコンポーネントファイルを作成します。
Link.tsx
type Props = {
url: string;
}
const Link = (props: Props) => {
if (props.url==="") {
return (<div/>)
}else{
return (
<div>
<a
target="_blank"
rel="noopener noreferrer"
href={props.url}>
View Cat!!!
</a>
</div>
)
};
}
export default Link;
- propsが引数で文字列なんですがPropsという一つのオブジェクトに突っ込むのがReact流
- TypeScriptなんでPropsの型定義も必要なんですよね、ここでtype定義するのは コレジャナイ感 がするのですが雰囲気理解なのでこれでヨシとします
- urlの値がなければ
<dev/>
を返すだけにしています
Linkコンポーネントを使う側も修正します
Top.tsx
import { useDispatch, useSelector } from 'react-redux';
import getCatAction from './getCatAction';
import { RootState } from './store';
import Link from './Link';
const Top = () => {
const dispatch = useDispatch()
const buttonAlert = () => {
dispatch(getCatAction())
}
const state = useSelector((state:RootState) => state.getCatReducer)
return (
<div>
<p>Cats rule the world.</p>
<button onClick={buttonAlert}>get cat</button>
<br/>
<label>{state.status}</label>
<br/>
<label>{state.url}</label>
<br/>
<Link url={state.url} />
</div>
)
}
export default Top;
どうでもいい修正ですが、最初に表示したとき init って表示されるのはもうわかったので消します
getCatReducer.ts
const initalState:CatState = {
status: "", // init を消す
url: "",
lastUpdated: Date.now()
}
動作確認
Very Good !
おつかれさまでした
いやーめんどくさい、フロントエンドって皆こんなめんどくさいのか
Discussion