🌊

React&TypeScriptでのreact-redux、redux-thunkお勉強メモ

2021/08/15に公開

フロントエンドのお勉強をしています。

とりあえず 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')
);

リクエスト投げる先

投げる先はコチラ

https://docs.thecatapi.com/

投げるといいかんじの猫画像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

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.statusstate.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