LoginSignup
76
57

More than 5 years have passed since last update.

Redux における "ActionCreator & Action" の Best Practice を考える

Last updated at Posted at 2017-09-05

:exclamation:注意:exclamation:
この記事はグダグダ記述を含みますが、結論までの前提も重要になると思っています。
しかし、結論だけ見たい方は下記を参照ください。

また、当記事では下手な日本語や技術的に歪曲した理解、英語の意訳を含む可能性があります。
そういったミスに、素直な指摘を投擲できる方のみ御覧ください。

「もっとこうしたらいいと思う!」「こっちのほうがクールだよ?」という意見がありましたら
是非是非お願いします!!


はじめに

こんにちは。
PCの背景に猫の画像を探していたら、にやけてしまって、変な目でみられてしまいました。
どうでもいいですね。

本稿では、ActionCreatorとActionのベストプラクティスを考えていきます。
あと、これは自分もミスするのですが、ActionCreatorとActionは別物なので注意が必要です。
ときにActionという一言で、混同した書き方をされることがあります。
そのため、文章がしつこいかもしれませんが、ActionとActionCreatorという主語を極力入れています。

また先述の通り、本稿はReduxからActionおよびActionCreatorという概念を重点的に切り取って話を進めていきます。
もし、Reduxで迷子になったら、下記ドキュメントがとてもいい道標になると思います。
僕は一時期、ここから画像を一枚引っ張り出してPCの壁紙にしていました。
https://github.com/reactjs/redux/issues/653
(ちなみに、https://github.com/reactjs/redux/issues/653#issuecomment-216844781 がいちばん好きです)

Action Creator

前置きはさておき、早速ですが。
Action Creator とは、Actionを作るためのロジックを記載するものになります。
よくあるのは下記のようなものですね。

actions/user.js
// Action creator
function setUserName(userName) {
  return { // Action
    type: 'SET_USER_NAME',
    UserName,
  };
}

このActionCreatorですが、どういったものなのか公式ドキュメントでは下記のようにあります。

Actions · Redux (参照日: 2017/09/01)
Action creators are exactly that—functions that create actions.
It's easy to conflate the terms “action” and “action creator,” so do your best to use the proper term.
In Redux action creators simply return an action.
(中略)
The dispatch() function can be accessed directly from the store as store.dispatch(), but more likely you'll access it using a helper like react-redux's connect().
You can use bindActionCreators() to automatically bind many action creators to a dispatch() function.
Action creators can also be asynchronous and have side-effects. You can read about async actions in the advanced tutorial to learn how to handle AJAX responses and compose action creators into async control flow.

Ref: http://redux.js.org/docs/basics/Actions.html#action-creators

意訳
ActionCreatorはActionを生成する機能です。
ActionとActionCreatorは混同しやすいので、言葉選びに注意してください。
ReduxのActionCreatorでは単にActionを返すものになります。
(中略)
dispatch()では、直接Storeへアクセスできますが、react-redux などの connect() でつなげて発火させることが多いです。
bindActionCreators() を使用すると、多くのActionCreatorを自動的にdispatch()関数にバインドできます。
ActionCreatorは非同期で、副作用もあります。
上級チュートリアルでは、非同期Actionについて読むことができます。
(AJAXレスポンスを処理し、ActionCreatorを非同期コントロールフローに組み立てる方法など)

なるほど。僕、似たようなこと書いちゃってましたね。(ActionとActionCreatorは違うよ!というところ。)
(中略)に書いてあったのはFluxとの違いでしたので, ごっそり割愛しています。

ここから読み取れるのは、ActionCreatorには同期的なActionCreatorと非同期的なActionCreatorの2パターンが存在することです。
同期的なActionCreatorは先述までのActionCreatorが当てはまります。
非同期的なActionCreatorといえば、例えば redux-thunk を導入しているとして下記のイメージです。

actions/user.js
// Action creator
function setUserName(userName) {
  return { // Action
    type: 'SET_USER_NAME',
    UserName,
  };
}

// Async Action creator
function setUserNameAsync() {
  return dispatch => {
    setTimeout(() => {
      dispatch(setUserName());
    }, 1000);
  };
}

意味ある例文とは言い難いですが、ユーザ名がセットされるタイミングをズラすようなActionCreatorを追加しました。
setTimeout() の存在が示すとおり、これは非同期的に動きます。

ここで問題だと感じているのは、同期的な処理と非同期的な処理が一つのフィールド(例えば、それはファイルかもしれないし、ActionCreatorという概念かもしれない)上にて同居していることです。
また、ここで書いた例ではそんなことはありませんが、非同期的な処理は往々にして「ビシネスロジック」であることが多いように感じます。
つまり、ここには「ビジネスロジック」に関わるActionCreatorと、「プレゼンテーションロジック」に関するActionCreatorが同居していることを示唆しています。
この異なるロジックの同居が、僕にとって居心地の悪さを感じさせているんです。

もちろん、この考え方と全く逆の考え方があることも知っています。
https://github.com/reactjs/redux/issues/1171#issue-123565061

また、ここに多様な意見が生まれることを物語るように、「意味概念的にActionCreatorは確立されたものでなく、一種のパターンだ」ということも言及されています。
https://github.com/reactjs/redux/issues/1171#issuecomment-197044922

故に、ActionCreatorのベストなんてものは存在しないのかもしれませんが、
少なくとも標準的な美しさを追求して書くことはできると信じています。
なので、ActionCreatorについては MY BestPractice として書きます。

Redux "Action Creator" MY Best Practice

ベストの形にするために、まず始めにやりたかったことは、「プレゼンテーションロジック」と「ビジネスロジック」および同期処理と非同期処理を明示的に分けることです。
簡単なところで、ファイルを分けてロジックを空間的に切り分けます。

よくある例としてThunkを持ってきました。が、さっそくですが、先述の通り、私はこのパターンが少し苦手です。
「プレゼンテーションロジック」と「ビジネスロジック」が同居していると思うからです。

actions/book.js_(A)
export function fetchBooks() {  
  return dispatch => {
    fetch("/books.json").then(response => { // <- Biz Logic (Async)
      const data = response.json();
      dispatch(receiveBooks(data)); // <- App Logic (Sync)
    })
    .catch(error => 
      dispatch({ type: types.FETCH_FAILED, error })
    );
  };
}

私なりの最適解はこのようにSET部をfetch部から引き剥がすことですが、Setter Actionが生成されます:frowning2:
(Action の BestPractice にて、setterActionがなぜ良くないと思うのか記述しています)

actions/book.js_(B)
export function fetchBooks() { // <- Biz Logic (Async)
  return dispatch => {
    return fetch("/books.json").catch(error => 
      dispatch({ type: types.FETCH_FAILED, error })
    );
  };
}

export function setBooks() { // <- App Logic (Sync)
  dispatch(fetchBooks()).then(response => {
    const data = response.json();
    dispatch({ type: types.SET_BOOKS, data });
  }
}

非同期処理で他にいい手段がないか探していると、よくredux-sageの話がされていることに気が付きました。
redux-sagaで非同期処理と戦う (参照日: 2017/09/02)
Reduxにおける非同期処理について、ActionCreator内またはsagaで行う (参照日: 2017/09/02)

下記がredux-saga のサンプルコードです。
詳細についてはこちらを参照いただけると助かります。

sagas.js
function* fetchUser(action) { // <- App Logic (Sync)
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({ type: "USER_FETCH_SUCCEEDED", user: user });
   } catch (e) {
      yield put({ type: "USER_FETCH_FAILED", message: e.message });
   }
}

function* rootSaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}

export default rootSaga;
main.js
// ... createStore(...)

// Saga を起動する
sagaMiddleware.run(rootSaga)
actions/book.js
export function fetchUser() {
  return {
    type: 'USER_FETCH_REQUESTED',
  }
}

まだ本格的に使用したわけではありませんが、非同期処理を裏で待たせておいて、Actionを検知したら非同期処理が動くということができます。
非同期処理を完全に書き分けられるのがすごくいいです。
また、同期処理内で、非同期だと意識することを減らすことができます。
すごく抽象的な言葉ですが、非同期処理をタスクとして切り分けられるので、ちぎっては投げ、ちぎっては投げ、ということができます。

私にとってはSAGAの考え方、書き方の方がフィットしたのです。
(なんと、日本語のドキュメントもあった!ありがとう!)
https://github.com/redux-saga/redux-saga/blob/master/README_ja.md

まだ周囲の環境はThunkで固められておりsagaの本格的な導入に至っていませんが、SAGAの書き方はこの悩みを解決してくれると信じています。

結論

  • 非同期処理と同期処理を分けて書く
    (見通しより、同期・非同期の機能分離を優先したいためです)

  • 明示的に非同期処理のロジックを切り出すために、sagaを利用する。
    (thunkを非難しているわけではありません。個人としてsagaの概念の方を好むというだけです)

  • 一見の見通しより、再利用性の向上と柔軟性を重視する
    (これは自分自身への気付きであり、対外的な結論ではないですが)

  • ActionCreatorにベストプラクティスはありません!

Action

続いて、Action とは、Store内の値を変更するために発行される処理になります。
actions/user.js でいうところの、下記の部分になります。

actions/user.js(Action抜粋)
// Action
{
  type: 'SET_USER_NAME',
  UserName,
};

このActionで何を書くのかということについて、公式ドキュメントでは下記のようにあります。

Actions · Redux (参照日: 2017/09/01)
Other than type, the structure of an action object is really up to you.
If you're interested, check out Flux Standard Action for recommendations on how actions could be constructed.

意訳
Typeが必須パラメータで、それ以外のパラメータは任意です。
興味のある人は Flux の標準アクションを参考にしてみてね。

Ref: http://redux.js.org/docs/basics/Actions.html

唐突に「任意です」と言われても、どういった値が推奨されるのか書かれておらず、
気になる人はFluxを見てね!とだけあり、あまりにも不親切です。
そして、Redux は Flux にインスパイアされて作られたものではあると思いますが、
概念的に完全に同じものでないこのは自明です。(違いがなければ、比較記事は存在できませんから)
と聞けば、より疑問が残ります。そのまま概念を Flux 標準に合わせていいのかな?とか。

ActionはActionCreatorと異なり、確立した意味概念を持ちます。
故に、共通認識的なベストプラクティスがあると信じています。

How Redux "Action" MY Best Practice

そこで、結局どんなActionを作ればいいのか? という話が出てきますが、
Dan Abramov(https://github.com/gaearon) によって、下記の言及がされています。

Twitter Thread @Dan Abramov (発言日: 2016/11/20, 参照日: 2017/09/01)
Indeed “glorified setters” is a very common misuse of Redux.
If your actions creator names start with set* and you often call multiple in a row, you might be missing the point of using Redux.
The point is to decouple “what happened” from “how the state changes”.
They are also inefficient and may lead to inconsistent UI.
Dispatching multiple times is an escape hatch and should be used sparingly.
Why don’t we explain this in the docs?
When we wrote them we did so for Flux users and assumed understanding of what Flux actions are.

Ref: https://mobile.twitter.com/dan_abramov/status/800310624009994240

意訳
Action を単なる Setter として使うのは、Redux を使う上でやってしまいがちなミスだ。
もし、Action の中に SET から始まるアクションが複数あるなら、Redux としての使い方のポイントがズレている(正しくない使い方をしている)。
Reduxでポイントとなるのは、「状態がどう変化したのか」ということから「何が起こったのか」という事象を切り離すことだ。
Setter というアクションは本来の目的を見失った象徴のようなものであって、2つの事象が融合してしまっているからこそ生まれてしまう。
また、そのような構成は非効率的であるし、UIとの不整合が起きてしまうかもしれない。
Setter については複数回 Dispatch することは、最後の切り札として使うのは控えたほうがいい。
なんでチュートリアルに書かないんだろうね?
Fluxの活動をしていたときはこのことを一番に書いたし、そのおかげでFluxではそんな過ちはなかったのに。

日本語でだけど、ココに書いたよ, Mr.Dan!見てくれてるかなぁ。。。とまぁ、脱線はさておき。
つまり、この記事の一番最初に書いたようなActionは不適切だったということになりますね。

actions/user.js【BAD】
// Action creators
function setUserName(userName) {
  return { // Action
    type: 'SET_USER_NAME', // <- this is SETTER...
    UserName,
  };
}

先程のTwitterThreadにて、適切な形を "Fluxのドキュメントに書いた" とあったので、そちらを見てみます。

flux-concepts#actions (参照日: 2017/09/01)
Actions define the internal API of your application.
They capture the ways in which anything might interact with your application.
They are simple objects that have a "type" field and some data.
Actions should be semantic and descriptive of the action taking place.
They should not describe implementation details of that action.
Use "delete-user" rather than breaking it up into "delete-user-id", "clear-user-data", "refresh-credentials" (or however the process works).
Remember that all stores will receive the action and can know they need to clear the data or refresh credentials by handling the same "delete-user" action.

Ref: https://github.com/facebook/flux/tree/master/examples/flux-concepts#actions

意訳
Actionはアプリケーションにおける内部APIとして定義する。
アプリケーション内でどういったデータのやり取りがあるのかを定義する。
またActionは、Typeと幾つかのデータを持った単純なオブジェクトとする。
Actionはそれ単体で意味を持つものであり、起こっている行動を説明するものでなければならない。
そのActionの実装の詳細を記述すべきではない。
「delete-user-id」、「clear-user-data」、「refresh-credentials」に分割するのではなく、「delete-user」を使用する。
このようにすることで、StoreがActionを受け取り、同じように「ユーザーの削除」Actionを処理してデータを消去して、情報を更新することができる。

ここに書いてある意図に合わせて、先程の BAD Action を書き換えてみます。
具体的には、Userに対しての変更のAPIを作るイメージで書いてみます。

今回は、編集したい部分をKeyで表現し、その際に変更したい値をuserで渡します。
こう書くことで、Store内のUserの名前が書き換えたくなっても、年齢を書き換えたくなっても。
どのような変更をしたかったとしても、このAction一つで対応できそうです。

actions/user.js【GOOD】
function editUser(user, key) {
  return { // Action
    type: 'EDIT_USER', // <- NOT Setter :-)
    key,
    user,
  };
}

なるほど。Actionは内部APIなんだ!と思って書くと、UIに全く依存することなく書けました。
仮にユーザ変更用のコンポーネントがあって、そこに対してのが増えても、UIにロックインされることなくActionを使えそうです。

ここまでで、十分に使いまわすことのできる再利用性の高いActionを作ることができました。
僕はこの段階で、十二分に要求を満たした。としてこれ以上の汎用性を切り捨てることがありますが、
そして、ここまできて、ようやく Flux標準 と書かれる記事に合わせてActionを書いても良さそうだな。と決心がつきます。
但し、Reduxは大体Fluxであって、概念的に完全に同じものでないということを念頭に置く必要があります。

 acdlite/flux-standard-action (参照日: 2017/09/01)
type
The type of an action identifies to the consumer the nature of the action that has occurred. By convention, type is usually a string constant or a Symbol. If two types are the same, they MUST be strictly equivalent (using ===).

payload
The optional payload property MAY be any type of value. It represents the payload of the action. Any information about the action that is not the type or status of the action should be part of the payload field.
By convention, if error is true, the payload SHOULD be an error object. This is akin to rejecting a promise with an error object.

error
The optional error property MAY be set to true if the action represents an error.
An action whose error is true is analogous to a rejected Promise. By convention, the payload SHOULD be an error object.
If error has any other value besides true, including undefined and null, the action MUST NOT be interpreted as an error.

meta
The optional meta property MAY be any type of value. It is intended for any extra information that is not part of the payload.

Ref: https://github.com/acdlite/flux-standard-action

仮に上記の標準に合わせて書いてみるとこんな感じでしょうか。

actions/user.js【GOOD】
function editUser(payload, error, meta) {
  return { // Action
    type: 'EDIT_USER',
    payload: {,
    error,
    meta,
  };
}

とても汎用性が高そうなActionができました。
処理中での validate error や, 変更のための DBサーバ上への変更アクセスの失敗 がこの直前にあったとしても、error のフラグ立てで対応できますね!!

とはいっても、Fluxドキュメントにあるように type 以外は任意です。
これは Redux の公式ドキュメントでも言及されていましたので、その通りに不要な値がある場合は取り除けばよさそうです。
仮に validate もかけない, DBサーバ上への変更アクセスもない Actionと想定すれば、errorはいらなさそうですね。

actions/user.js【GOOD】
function editUser(payload, error, meta) {
  return { // Action
    type: 'EDIT_USER',
    payload,
    meta,
  };
}

結論

あとがき

今回は、Actionに焦点を絞って会話を進めました。
本来であればReducerの話も並行してしなければいけないのかなと思っていますが、
記述範囲とページの長さと僕の元気の関係上、そこを取っ払って話をすすめました。

Actionが使う初期値はReducer内で定義するため、ReducerとActionは切っても切れない関係にあり、独立して存在することはできません。
(これについてReduxコミュニティ内で議論を見た気がするのですが、ちょっと参照先を見失いました。。。)

その話もいずれ書きたいです。時間が取れれば、という感じですが。

また、これら、Reduxの踏み込んだ議論は下記のスレッドで長くに渡り議論されています。
(確認したときには 2015/12 〜 2017/03 まで、 約1年半もの間議論が続いていました)
興味深いコメントも多く、理解も深まると思うので、一読以上するといいかもしれません!
https://github.com/reactjs/redux/issues/1171

また、参考になりそうな記事を紹介してもらいました!こちらは、まだ読書中です。
http://blog.isquaredsoftware.com/2017/01/idiomatic-redux-thoughts-on-thunks-sagas-abstraction-and-reusability/

redux exp. https://github.com/reactjs/redux/tree/master/examples

76
57
1

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
76
57