KDE BLOG

Webデザインやコーディングについて書いています

【webpack速習】vol.5: Hot Module Replacement

webpack.js.org

上記ページの要点をまとめていきます

  • 概要
  • HMRはどのように機能するか
    • 1. アプリケーションにおいて
    • 2. コンパイラにおいて
    • 3. モジュール内において
    • 4. ランタイム(実行)時において
    • はじめかた
  • HMRの使い方
    • HMRを有効にする
    • webpack.config.js で設定しない場合
    • HMRが難しい場合がある
  • CSSのHMR
  • まとめ
続きを読む

【webpack速習】vol.4: Development

下記ページの要点になります。 webpack.js.org

開発時に役立つ機能のガイドページになります。
コードを自動的にコンパイルし、簡単な開発サーバを実行する内容です。

  • development mode
  • source maps を使う
  • 開発ツールの選択
    • watch モードを使う
    • webpack-dev-server を使う
    • webpack-dev-middleware を使う
      • webpack-dev-middleware と express サーバを組み合わせた例
続きを読む

【webpack速習】vol.3: Output Management

下記ページの要点をまとめていきます。

webpack.js.org

ざっくりいうと、出力されたバンドルファイルの読み込みを自動で行うようにするためにhtmlの生成を動的にするガイドです。

  • 概要
  • 準備
    • エントリーポイントとアウトプットの設定を修正
  • HtmlWebpackPlugin
    • テンプレート機能を試してみる
  • clean-webpack-plugin で /dist フォルダの掃除
続きを読む

【webpack速習】vol.2: Asset Management

今回は下記ページの要点をまとめていきたいと思います。

webpack.js.org

  • CSSを読み込む
    • 補足
  • 画像の読み込み
  • フォントファイルを読み込む
  • csvxmlなどのデータファイルを読み込む
  • グローバルなアセット
続きを読む

【webpack速習】vol.1: 概要 & Getting Started

個人ではなんとなくコピペで使っていたwebpack。
会社業務で使っている webpack のビルドを早くしたいのだけれど、ちゃんと webpack について理解しておらず設定ファイルをうまくいじれないのが悔しいので、ちゃんと1から学ぼうと思います。
volいくつまで続くかわかりませんが、ひとまず今回は、webpackの概要と、実際に動かしてみるところまで。

基本的なスタンスとしては、参考情報はwebpackの公式ドキュメントをベースにしつつ、その他随時ググった記事を頼りにしていきたいと思います。
うまくまとめるというよりはログを残す感じになるので雑な内容になるかもしれませんのであしからず。

  • webpackとは何か
  • webpackの本質
  • webpackと似たもの
  • webpackの基本要素
  • 実際に動かす
    • "private": true の意味
    • npm install --save ? npm install --save-dev ?
    • config ファイルを作成
  • 参考
続きを読む

Redux-thunkを使ったReduxの非同期処理

前回の記事では同期的なTODOアプリを例としてReact + Reduxの基本的な使い方を学びなおしました。
今回は非同期処理の扱い方を勉強してサンプルのアプリケーションを作ったので、要点などをメモしておきます。

Reduxでの非同期処理について

実際のアプリケーションではAPI通信など非同期処理が必ずと言っていいほど入ってくると思います。 たとえば fetch() を使って何かデータを取ってくる Action creator を定義したいとします。

const fetchSomeThing = (url) => {
  return fetch(url)
    .then(res => res.json())
    .then(json => {
      return {
         type: 'FETCH_DATA',
         payload: {
           response: json
         }
      }
    }); 
};

fetchSomeThing('APIのURL').then(response => store.dispatch(response));

上記のようにすればfetchした結果で得たActionをdispatchすることは可能ですが、正直使いにくいです。
できることなら、同期的な処理と同じような感じで、store.dispatch(fetchSomeThing('APIのURL')) とできると嬉しいですね。

さらにAPIコールする場合、ローディング中やローディング完了後、さらにエラーが発生した場合などの考慮をしなくてはなりません。
つまり下記の Action と Action creatror が必要になります。

  • リクエスト開始を知らせるAction
    isFetchig: true などにしてローディング中の対応をします

例)

const requestData = url => ({
  type: 'REQUEST_DATA',
  payload: {
    url
  }
});
  • リクエスト成功を知らせるAction
    isFetching: false のようにリセットして、取得したデータを任意のpropertyに追加します

例)

const receiveData = response=> ({
  type: 'RECEIVE_DATA',
  payload: {
    response
  }
});
  • リクエスト失敗を知らせるAction
    isFetching: false; isError: true などのようにしてエラーの対応をします

例)

const failReceiveData= error => ({
  type: 'FAIL_RECEIVE_DATA',
  error: true,
  payload: error
});

これを先ほどの、fetchSomeThing に当て込んでみます。

const fetchSomeThing = url => {
  return requestData(url);

  // 詰みました orz
  fetch(url).then(res =>...
}

このように上手く処理できません。
同期的なAction creatorを非同期処理の中でうまく使えるようにするには、Reduxのミドルウェアである Redux-thunk を使うのが一般的です。

Redux-thunkとは

Redux-thunkを使用すると、Action creator はオブジェクトの代わりに 関数を返すことができるようになります。
関数を返す Action creatorThunk となります。
Thunkが返す関数は、Redux-thunkのミドルウェアによって実行されます。
この関数は、Reducerのように純粋な関数でなくても良く、つまりAPI呼び出しなどの非同期を伴ったりしても問題ありません。

またこの関数は dispatch を第1引数に持たせることができるため、関数内で Action を Dispatch することができます。
(第2引数には getState を持たせることができます。これにより、関数内で Store の state を見ることができます)

この特性を生かすと先ほどの fetchSomeThing は下記のように記述することができます。

const fetchSomeThing = (url) => {
  return (dispatch) => {
    // リクエスト開始のActionをdispatch
    dispatch(requestData(url));

    return fetch(url)
      .then(res => {
        if (!res.ok) {
          return Promise.resolve(new Error(res.statusText));
        }
        return res.json();
      })
      .then(json => {
        // レスポンスの受け取り(リクエスト成功)のActionをdispatch
        dispatch(receiveData(json))
      })
      .catch(error => {
        // リクエスト失敗のActionをdispatch
        dispatch(failReceiveData(error));
      });
  }
};

このように非同期の中でも、Actionをdispatchすることでstoreのstateを更新するのは変わりません。

ちなみに上記はPromiseを返していますので、呼び出し側では then() などで処理を直列で実行できます。

store.dispatch(fetchSomeThing('APIのURL'))
  .then(() => /* something do... */);

Redux-thunkの取り込み方

Redux-thunkはミドルウェアなので、createStore() 実行時に、applyMiddleware というエンハンサー(強化プログラム)を使って取り込みます。

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from './reducers'

const middlewares = [thunk]; // 他にミドルウェアを追加する場合はここに入れる

const store = createStore(
  rootReducer,
  applyMiddleware(...middlewares)
);

以上が基本的なRedux-thunkを使った非同期処理のやり方です。

サンプルアプリケーション

React + Redux + Redux-thunk を使って、公式チュートリアルを真似た簡単なサンプルを作りました。

仕様

  • ページロードしたら、タグを取得するAPI(ただ ['react', 'vue', 'angular'] を返すだけ)をコールしてボタンを表示
  • ボタンをクリックすると、Qiitaのそのタグ情報を取得するAPIをコールして、タグ情報を表示
  • 一度取得したタグ情報は保存しておき、「再読み込み」をクリックしない限り、再度APIコールしない

stateの形

やはりstateの形からデザインするのがよさそうです。

{
  // タグ取得に関するstate
  tags: {
    tagAll: ['react', 'vue', 'angular'],
    isFetching: false,
    isError: {
      status: false,
      error: null
    },
    selectedTag: 'react',
  },

  // タグの詳細情報に関するstate
  tagDatas: {
    react: {
      isFetching: false,
      isError: {
        status: false,
        error: null
      },
      shouldUpdate: false,
      lastUpdated: 137983721
      responseData: {
        "followers_count": 298,
        "icon_url": "https://s3-ap-northeast-1.amazonaws.com/qiita-tag-image/e6867d326364bb2498f72f152c92408bf457de8c/medium.jpg?1426679594",
        "id": "react.js",
        "items_count": 262
      }
    }
  }
}

/**
 * 初期stateはこんな感じ
 */
{
  tags: {
    tagAll: [],
    isFetching: false,
    isError: {
      status: false,
      error: null
    },
    selectedTag: ''
  },
  tagDatas: {}
}

Action type

次にAction Typeを考えます。

  • REQUEST_TAGS
    • タグ一覧をリクエス
  • RECEIVE_TAGS
    • タグ一覧を受け取り
  • FAIL_REQUEST_TAGS
    • タグ一覧のリクエスト失敗
  • SELECT_TAG
    • タグを選択
  • REQUEST_TAG_DATA
    • タグの詳細情報をリクエス
  • RECEIVE_TAG_DATA
    • タグの詳細情報を受け取り
  • REFRESH_TAG_DATA
    • タグの情報を再読み込み
  • FAIL_REQUEST_TAG_DATA
    • タグの詳細情報のリクエスト失敗

Action creator

/**
 * Action Creator
 */
export const requestTags = () => ({
  type: REQUEST_TAGS
});

export const receiveTags = (json) => ({
  type: RECEIVE_TAGS,
  payload: {
    response: json
  }
});

export const selectTag = (tag) => ({
  type: SELECT_TAG,
  payload: {
    tag
  }
});

export const requestTagData = (tag) => ({
  type: REQUEST_TAG_DATA,
  payload: {
    tag
  }
});

export const receiveTagData = (tag, json) => ({
  type: RECEIVE_TAG_DATA,
  payload: {
    tag,
    response: json
  }
});

export const refreshTagData = (tag) => ({
  type: REFRESH_TAG_DATA,
  payload: {
    tag
  }
});

export const failRequestTags = (error) => ({
  type: FAIL_REQUEST_TAGS,
  error: true,
  payload: {
    error
  }
});

export const failRequestTagData = (tag, error) => ({
  type: FAIL_REQUEST_TAG_DATA,
  error: true,
  payload: {
    tag,
    error
  }
});

/**
 * Thunk
 * タグ一覧を取得する
 */
export const fetchTags = () => {
  return (dispatch) => {
    dispatch(requestTags());
    return fetch(API_GENRE)
      .then(res => {
        if (!res.ok) {
          return Promise.resolve(new Error(res.statusText));
        }
        return res.json();
      })
      .then(json => dispatch(receiveTags(json)))
      .catch(error => dispatch(failRequestTags(error)));
  }
};

/**
 * Thunk
 * タグの詳細情報を取得する
 */
const fetchTagData = (tag) => {
  return (dispatch) => {
    dispatch(requestTagData(tag));
    return fetch(API_QIITA_TAGS + tag)
      .then(res => {
        if (!res.ok) {
          return Promise.resolve(new Error(res.statusText));
        }
        return res.json();
      })
      .then(json => dispatch(receiveTagData(tag, json)))
      .catch(error => dispatch(failRequestTagData(tag, error)));
  }
}

/**
 * タグの詳細情報を取得するか判定
 * (すでに対象のタグの詳細情報を取得済みであればfalseを返す)
 */
const shouldFetchTagData = (tag, state) => {
  if (state.tagDatas[tag] === undefined || state.tagDatas[tag].shouldUpdate) {
    return true;
  }
  if (state.tagDatas[tag].isFetching) {
    return false;
  }
  return false;
};

/**
 * Thunk
 * タグの詳細情報を必要であれば取得する
 */
export const fetchTagDataIfNeeded = (tag) => {
  return (dispatch, getState) => {
    if (shouldFetchTagData(tag, getState())) {
      return dispatch(fetchTagData(tag));
    }
  }
};

ここでのポイントになるのは、fetchTagDataIfNeeded でしょうか。
このアプリケーションでは、一度タグ詳細情報を取得している場合、再読み込みボタンを押さない限り再度APIコールを行いません。
そのロジックを呼び出し側で行うと、view側で条件分岐などをして fetchTagData を実行する処理を書かなくてはなりません。
しかしそのロジックをAction Creatorに持たせれば、view側では気にせずにそのAction Creatorをdispatchするだけというシンプルな作りになります。

またThunkからThunkをdispatchできるのもThunkの使い勝手が良いところです。

Reducer

▼ /src/reducers/index.js

import { combineReducers } from 'redux';
import tags from './tags';
import tagDatas from './tagDatas';

const rootReducer = combineReducers({
  tags,
  tagDatas
});

export default rootReducer;

▼ /src/reducers/tags.js

import * as Actions from '../actions';

function selectedTag(state = '', action) {
  switch (action.type) {
    case Actions.SELECT_TAG:
      return action.payload.tag;
    default:
      return state;
  }
}

function tagsIsFetching(state = false, action) {
  switch (action.type) {
    case Actions.REQUEST_TAGS:
      return true;
    default:
      return false;
  }
}

function tagsIsError(state = {
  status: false,
  error: null
}, action) {
  switch (action.type) {
    case Actions.FAIL_REQUEST_TAGS:
      return {
        ...state,
        status: true,
        error: action.payload.error
      };
    default:
      return {
        ...state,
        status: false,
        error: null
      };
  }
}

/**
 * タグ一覧 のstateを管理するReducer
 */
export default function tags(state = {
  tagAll: [],
  isFetching: false,
  isError: {
    status: false,
    error: null
  },
  selectedTag: ''
}, action) {
  switch (action.type) {
    case Actions.REQUEST_TAGS:
      return {
        ...state,
        isFetching: tagsIsFetching(state.isFetching, action)
      };
    case Actions.RECEIVE_TAGS:
      return {
        ...state,
        tagAll: action.payload.response,
        isFetching: tagsIsFetching(state.isFetching, action)
      };
    case Actions.SELECT_TAG:
      return {
        ...state,
        selectedTag: selectedTag(state.selectedTag, action)
      };
    case Actions.FAIL_REQUEST_TAGS:
      return {
        ...state,
        isError: tagsIsError(state.isError, action),
        isFetching: tagsIsFetching(state.isFetching, action)
      };
    default:
      return state;
  }
}

▼ /src/reducers/tagDatas.js

import * as Actions from '../actions';

function tagData(state = {
  isFetching: false,
  isError: {
    status: false,
    error: null
  },
  shouldUpdate: false,
  responseData: {}
}, action) {
  switch (action.type) {
    case Actions.REQUEST_TAG_DATA:
      return {
        ...state,
        isFetching: true,
        shouldUpdate: false
      };
    case Actions.RECEIVE_TAG_DATA:
      return {
        ...state,
        isFetching: false,
        isError: {
          status: false,
          error: null
        },
        lastUpdate: Date.now(),
        responseData: action.payload.response
      };
    case Actions.REFRESH_TAG_DATA:
      return {
        ...state,
        shouldUpdate: true
      };
    case Actions.FAIL_REQUEST_TAG_DATA:
      return {
        ...state,
        isFetching: false,
        isError: {
          status: true,
          error: action.payload.error
        }
      };
    default:
      return state;
  }
}

/**
 * タグの詳細情報を管理するReducer
 */
export default function tagDatas(state = {}, action) {
  switch (action.type) {
    case Actions.REQUEST_TAG_DATA:
    case Actions.RECEIVE_TAG_DATA:
    case Actions.REFRESH_TAG_DATA:
    case Actions.FAIL_REQUEST_TAG_DATA:
      return {
        ...state,
        [action.payload.tag]: tagData(state[action.payload.tag], action)
      };
    default:
      return state;
  }
}

少々複雑なstateになるため、Reducer を適当な大きさに分けています。
ここでのポイントは、tag.jstagData.js 内で Reducer合成 を行っている点です。

stateがネストしている場合、適度にReducer合成を行うことで、Reducerごとに責任を分けることができて管理しやすくなる利点があります。
特定のStateの状態がおかしいとき、そのReducerに注目すればトラブルシューティングがしやすいです。

Store

▼/src/store/configureStore.js

import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';

const logger = createLogger();
const middlewares = [thunk, logger];

export default function configureStore(pleloadState) {
  return createStore(
    rootReducer,
    pleloadState,
    applyMiddleware(...middlewares)
  );
};

redux-thunk の他に、デバッグ用のログをとる redux-logger を使用します。
ここで要注意なのが、ミドルウェアを記述する順番です。 const middlewares = [thunk, logger]; ではなく、const middlewares = [logger, thunk]; と、redux-logger を先に入れてしまうと、console上で undefined のActionが実行される事象が発生する可能性があります。
※Redux-thunk の リポジトリに issue として挙がっていました。 github.com

Storeを作ったらUIは別としてデータロジックのテストができます。

import configureStore from './store/configureStore';
import * as Actions from './actions';

const store = configureStore();
store.dispatch(Actions.fetchTags())
  .then(() => {
    store.dispatch(Actions.selectTag('vue'));
    store.dispatch(Actions.fetchTagDataIfNeeded('vue'));
  });

開発者ツールのコンソール上で意図した実行結果になっているか確認します。

問題なさそうです!
あとはコンポーネントを作り、Reduxとつなげる処理をします。

コンポーネント設計

コンポーネントは下記のように分けることにしました。
赤がコンテナコンポーネント(Storeと繋ぐコンポーネント)、青がプレゼンテーショナルコンポーネントです。

適当に作ったので見にくいですが、階層的には下記のような感じです。

コンポーネント作成

ここからあとはひたすらにコンポーネントを作っていきますが、エントリーポイントとなるjsファイルの内容は下記になります。

▼ /src/index.js

import React from 'react';
import { render } from 'react-dom';
import configureStore from './store/configureStore';
import { Provider } from 'react-redux';
import App from './components/App';

const store = configureStore();

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

プレゼンテーショナルコンポーネント

propsから受けとる値を元に、見た目を作るコンポーネントです。

▼ /src/components/Btn.jsx

import React from 'react';
import styled from 'styled-components';

const Btn = ({ tag, children, onClick }) => {
  const handleClick = () => onClick(tag);
  return (
    <Wrapper>
      <ItemWrapper onClick={handleClick}>{children}</ItemWrapper>
    </Wrapper>
  );
};

const Wrapper = styled.li`
  // styleのため省略
`;

const ItemWrapper = styled.button`
  // styleのため省略
`;

export default Btn;

▼ /src/components/BtnGroup.jsx

import React, { Component } from 'react';
import styled from 'styled-components';
import Btn from './Btn';
import {
  fetchTags,
  selectTag,
  fetchTagDataIfNeeded,
} from '../actions';

class BtnGroup extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  componentDidMount() {
    const { dispatch } = this.props;
    dispatch(fetchTags());
  }

  handleClick(tag) {
    const { dispatch } = this.props;
    dispatch(selectTag(tag));
    dispatch(fetchTagDataIfNeeded(tag));
  }

  render() {
    const { tagAll, isFetching, isError } = this.props;
    return (
      <Wrapper>
        {isFetching &&
          <Message>Now Loading...</Message>
        }
        {isError.status &&
          <Message>{isError.error.message}</Message>
        }
        {tagAll.length > 0 &&
          <React.Fragment>
            <ListHead>タグ:</ListHead>
            <ListWrapper>
              {tagAll.map(tag => <Btn key={tag} tag={tag} onClick={this.handleClick}>{tag}</Btn>)}
            </ListWrapper>
          </React.Fragment>
        }
      </Wrapper>
    )
  }
}

const Wrapper = styled.div`
  // styleのため省略
`;

const Message = styled.p`
  // styleのため省略
`;

const ListWrapper = styled.ul`
  // styleのため省略
`;

const ListHead = styled.h2`
  // styleのため省略
`

export default BtnGroup;

▼ /src/components/Head.jsx

import React from 'react';
import styled from 'styled-components';

const Head = ({ tag }) => (
  <Wrapper>
    Qiitaの記事タグ「{tag}」の詳細情報
  </Wrapper>
);

const Wrapper = styled.h1`
  // styleのため省略
`;

export default Head;

▼ /src/components/LastUpdate.jsx

import React from 'react';
import styled from 'styled-components';

const LastUpdate = ({ lastUpdate }) => {
  return (
    <Wrapper>
      最終更新日時: {new Date(lastUpdate).toLocaleTimeString()}
    </Wrapper>
  );
};

const Wrapper = styled.span`
  // styleのため省略
`;

export default LastUpdate;

▼ /src/components/RefreshLink.jsx

import React from 'react';
import styled from 'styled-components';

const RefreshLink = ({ onClick }) => (
  <Wrapper href="#" onClick={onClick}>再読み込み</Wrapper>
);

const Wrapper = styled.a`
  // styleのため省略
`;

export default RefreshLink;

▼ /src/components/Details.jsx

import React from 'react';
import styled from 'styled-components';

const Details = ({ followers_count, icon_url, items_count }) => (
  <Wrapper>
    <ItemWrapper>フォロワー数: {followers_count}</ItemWrapper>
    <ItemWrapper>アイコン画像: <Img src={icon_url} /></ItemWrapper>
    <ItemWrapper>記事数: {items_count}</ItemWrapper>
  </Wrapper>
);

const Wrapper = styled.ul`
  // styleのため省略
`;

const ItemWrapper = styled.li`
  // styleのため省略
`;

const Img = styled.img`
  // styleのため省略
`;

export default Details;

▼ /src/components/TagDetail.jsx

import React from 'react';
import styled from 'styled-components';
import Head from './Head';
import LastUpdate from './LastUpdate';
import RefreshLink from './RefreshLink';
import Details from './Details';
import { refreshTagData, fetchTagDataIfNeeded } from '../actions';

const TagDetails = ({ selectedTag, tagDatas, dispatch }) => {
  const selectedTagData = tagDatas[selectedTag];
  if (!selectedTagData) {
    return null;
  }
  const { isError, isFetching, lastUpdate, responseData } = selectedTagData;
  const onRefresh = (e) => {
    e.preventDefault();
    dispatch(refreshTagData(selectedTag));
    dispatch(fetchTagDataIfNeeded(selectedTag));
  };
  const hasResponseData = Object.keys(responseData).length > 0;

  return (
    <Wrapper>
      <Head tag={selectedTag} />
      {isFetching && !hasResponseData &&
        <Message>Now loading...</Message>
      }
      {isError.status && 
        <Message error>{isError.error.message}</Message>
      }
      <div style={{ opacity: isFetching ? 0.5 : 1 }}>
        {lastUpdate && 
          <LastUpdate lastUpdate={lastUpdate} />
        }
        <RefreshLink onClick={onRefresh} />
        {hasResponseData &&
          <Details {...responseData} />
        }
      </div>
    </Wrapper>
  );
};

const Wrapper = styled.div``;
const Message = styled.p`
  // styleのため省略
`;

export default TagDetails;

▼ /src/components/App.jsx

import React, { Component } from 'react';
import BtnGroup from '../containers/BtnGroup';
import TagDetail from '../containers/TagDetail';

class App extends Component {
  render() {
    return (
      <div>
        <BtnGroup />
        <TagDetail />
      </div>
    );
  }
};

export default App;

コンテナコンポーネント

コンテナコンポーネントは Store とつながることで state を参照できます。
mapStateToProps を使って、stateを元に子コンポーネントへpropsで必要な情報を渡します。
基本的にはコンテナコンポーネントには jsx(見た目)を書くことはありません。

▼ /src/containers/BtnGroup.jsx

import { connect } from 'react-redux';
import BtnGroup from '../components/BtnGroup';

function mapStateToProps(state) {
  const { tagAll, isFetching, isError } = state.tags;
  return {
    tagAll,
    isFetching,
    isError
  };
}

export default connect(mapStateToProps)(BtnGroup);

▼ /src/containers/TagDetail.jsx

import { connect } from 'react-redux';
import TagDetail from '../components/TagDetail';

function mapStateToProps(state) {
  const { tagDatas, tags } = state;
  return {
    selectedTag: tags.selectedTag,
    tagDatas
  }
}

export default connect(mapStateToProps)(TagDetail);

実装してみて気づいたのですが、mapDispatchToProps でコールバック関数も props で渡すことが可能ですが、コールバック関数を追加したい場合など修正が発生したときに、呼び出し元と呼び出し先の両方でコードを修正しなくてはならず、ちょっと面倒に感じました。
ケースバイケースかと思いますが、dispatchはそのままpropsから使って、呼び出し側でアクションを選択した方が良いかと感じました。

完成版

以上で完成になりますが、一部 styled-component のスタイルなど省略しています。
完全版は下記リポジトリになります。

create-react-app2/study_thunk at master · kde-space/create-react-app2 · GitHub

まとめ

  • Redux のミドルウェアである Redux-thunk を使うと非同期の Action も処理できる
  • Redux-thunkを使うと、ただのオブジェクトではなく、dispatchgetState を引数に取る関数を返す Action Creator (Thunk)を作ることができる
    • 返された関数は Redux-thunk のミドルウェアによって実行される
      • この関数は非同期のAPIコールなどの副作用を持たせることができる(純粋関数でなくて良い)
      • この関数は任意のタイミングで Action を dispatch することができる
      • Thunk の中から Thunk を dispatch することができる
      • 最終的に Actionをdispatch できれば state が更新される原則は変わらない
  • 実装に進む前に State の形を考えた方がスムーズ
  • APIなどの非同期通信の場合、3つの Action を用意する
    • リクエスト開始の Action
    • レスポンス受け取り(リクエスト成功)の Action
    • リクエスト失敗の Action
  • Reducer は 適度に分割して Reducer合成を行った方が見やすくなる
  • Redux-logger は、applyMiddleware() の最後に渡した方が良い

まだ経験不足なので誤り、ご指摘あればぜひお願いします。

参考

React + Redux の基本的な使い方

久しぶりにReduxを使ったら使い方をすっかり忘れていました。
改めて勉強しなおしたのでメモしておきたいと思います。

  • Reduxとは
    • Reduxのメリット
    • 3つの原則
      • 1. 真実の出所は1つ(Single source of truth)
      • 2. stateは読み込み専用(State is read-only)
      • 3. 変化は純粋(副作用のない)関数で作られる(Changes are made with pure functions)
  • Reduxの要素
    • Action
    • Action creator
    • Reducer
      • combineReducers
    • Store
      • Storeの作成方法
  • Reactと使う
  • まとめ
  • 参考
続きを読む