読者です 読者をやめる 読者になる 読者になる

かえるの井戸端雑記

開発日誌的な記事だったり備忘録だったり。まとめ記事と言うよりは、七転八倒の様子を小説みたいに読んで眺めてもらえればと。

redux-sagaを触ろうとしてそれ以前に整理しまくった話

非同期処理を扱いたい

とはいえreduxというStoreをうまく使うために作られたframeworkがあるせいでdataの更新とかここにすごく関わってくるので不用意に出来ない。さて何をどうすればいいのか。

(今回派手に迷走します)

libraryがいろいろ

で、調べてみるとreact-sagaといもうのがあった。ちなみにredux-sagaというのもある。redux環境下で使うならたぶんredux-sagaだよね。dataの取り回し周りに関わってくるだろうから……。

ああredux-thunkというのでもいけるらしい。

どうもこれらは非同期処理をreduxに入れ込んであげたいという要求の元に生まれたみたい。 で、違いは何さ。

qiita.com

おおう。redux-promisというのも出てきた。ええいかまうものか。ふるいにかけてくれるわ(まあそのためにはまず知識の海でおぼれてこないといけないんだけど……)。

非同期処理をコンテナ(またはそこで使うモジュール)に書くだけ。 今は、非同期処理をするためのredux middlewareを使ってないです。 過去記事(react, redux周りのパッケージ選定とKPT[2016-05-27現在])にて、この辺についてもなんやかんやと言っているけど、今はthunkもpromiseもsagaも使っていない。その実装が凄くシンプルで気に入ってるので紹介します。

ってここの人は結局使わないという結論に至ったのか。それはそれでわかりやすい。

Promseとasync/awaitを覚えればいいと。ほうほう。

まあここまで触れてみて思ったけど確かに無理してreact-routerとか使う必要ないなあとは思い始めている。reduxもそう。

なんだろう。これ僕の性格がframeworkとあわないという事なのだろうか。そんなひどい。でも体系的なところは自分で考えたい、自分のセンスでやりたいという欲求は確かにある。

もうちょっと調べてみて使うかどうか考えてみよう。

疑問はこのあたり。

  • redux#bindActionCreatorsってなんぞや
  • Promseとasync/awaitってなんぞや
  • redux-sagaの使い方
  • redux-thunkの使い方
  • redux-promiseの使い方

redux#bindActionCreatorsとはなんぞや。

qiita.com

Action を追加するたびに connect に追加するのはたいへんなので、bindActionCreators を使って自動的にマッピングします。

ああなるほど。

……追加するのか?

1秒後に何かしたいなど、非同期で Action を実行したい場合は、ActionCreator で function (dipatch) 型の function を返します。

なるほど。あー。get/postなどはそういう振る舞いになるのか。action相当なのか。

actionでよくない? いや非同期処理だからstoreの更新とかのtimingを考えると、となるとそうか、面倒なことになってくるのか。 だからmiddlewareにして組み込んでいると。

このあたりをうまくやろうとするとpromiseとかasyncとかawaitとか出てくるんだろうなあ。

promise、async、await

qiita.com

promise非同期処理をcallbackじゃなくてchain状に実装するためのlibraryか。

sbfl.net

Promiseを利用することで非同期処理を簡単に書けることはわかりました。しかし書き方が少し特殊です。そこで非同期処理をもっと簡潔に書けるように、async/awaitという機能が導入されました。

で、先を見てみるとasyncで修飾した関数ならawaitでpromiseを同期的な処理に見せかけることも出来る、と。

余談

あ、reduxの振る舞い、ここのものが一番わかりやすい図だった。

mid0111.hatenablog.com

redux-saga

さてredux-sagaの話を読んで見よう。 理論部分ならこっちのpageの方がわかりやすい。

qiita.com

redux-sagaは「タスク」という概念をReduxに持ち込むための支援ライブラリです。 ここで言うタスクというのはプロセスのような独立した実行単位で、それぞれが別々に並行して動作します。redux-sagaはこのタスクの実行環境を提供します。それに加えて非同期処理をタスクとして記述するための道具立てである 「作用(Effects)」 と非同期処理を同期的に書き下す手段も提供してくれます。

あー。独立したtaskのような振る舞いを定義できればそこで非同期処理だろうがなんだろうが組み込めるもんなあ。というか非同期かどうかとか考えなくて良くて、redux-sagaでwrapされている限り、redux-sagaの方でtaskの同期周りは気にしてあげればいい、と。

読み終わり。なんとなくやりたいことはわかった。わかったけどこれ、この説明だけだと、起動時に一度だけしか実行できない?

github.com

ああ違うのか。reducerに登録している以上、stateとactionの対なわけで、特定のstateを書き込むことで普通に実行されるか。

kickできるのが最初に登録したtaskに限られる、というだけ。 でもそれはredux-sagaの基本的な考え方なので別にいい。というかそういう追加する仕組みを利用して書き上げたのがredux-thunk。

ふむ。ふむふむ。 使うならredux-sagaが趣味に合いそう。これもまた使うだけ使ってみよう。

redux-sagaを使う前に環境構築

redux-sagaはもう入っている。問題はtestだ。どこかにjson file置けばwebpack-dev-serverってjsonとして出してくれるかな……。

qiita.com

なるほどwebpac-dev-serverでproxy。そういう手が。

いやそうではない。

www.thejoemorgan.com

あーなるほど。json-serverを別に立てててproxyしろという話か。なるほど。

webpack-dev-serverとjson-serverの連携

npm install --save-dev json-serverでinstall。

適当なfileを/pages/api/db.jsonとして用意する。

% cat ./pages/api/db.json
{
  "name": "test",
  "version": "0.1.0",
  "main": "index.js"
}

npm run start:apiで起動できるようにpackage.jsonを改造。

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config config/webpack-production.config.js --progress",
    "build:debug": "webpack --config config/webpack.config.js --progress",
    "start": "webpack-dev-server --config config/webpack-dev.config.js --watch --progress --inline",
    "start:api": "json-server --watch pages/api/db.json --port 3004"
  }

config/webpack-dev.config.jsに以下のようなproxyの設定を追加。

  proxy: {
    '/api': {
      target: 'https://localhost:3004',
      secure: false
    }
  },

あとはserverを並行起動すればいいんだけど、確かそんなmoduleがあったはず。前にexpressとかいじってたときに見つけてこれでいいじゃんと思ってがっくりした記憶がある。

npm-run-all と concurrently を試す – アカベコマイリ

これこれ。npm-run-allとconcurrently。どっちにしようかな。

npm-run-allでいいか。npm install --save-dev npm-run-allでinstall。

で、package.jsonのscriptsを改造して、

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config config/webpack-production.config.js --progress",
    "build:debug": "webpack --config config/webpack.config.js --progress",
    "start:webpack": "webpack-dev-server --config config/webpack-dev.config.js --watch --progress --inline",
    "start:api": "json-server --watch pages/api/db.json --port 3004",
    "start": "npm-run-all -p start:*"
  }

これでどうじゃろ。

  Type of "name" (string) in pages/api/db.json is not supported. Use objects or arrays of objects.

駄目だった。jsonにその名前を入れてはいけないらしい。というかdb.jsonには決められた形式があったのか。最初のkeyがroutingで、以降がresponseとなる模様。

ではdb.jsonを以下のように修正。

{
  "test": {
    "name": "test",
    "version": "0.1.0",
    "main": "index.js"
  }
}

これでどうだ。

Invalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema.
 - configuration has an unknown property 'proxy'. These properties are valid:
   object { amd?, bail?, cache?, context?, dependencies?, devServer?, devtool?, entry, externals?, loader?, module?, name?, node?, output?, performance?, plugins?, profile?, recordsInputPath?, recordsOutputPath?, recordsPath?, resolve?, resolveLoader?, stats?, target?, watch?, watchOptions? }
   For typos: please correct them.
   For loader options: webpack 2 no longer allows custom properties in configuration.
     Loaders should be updated to allow passing options via loader options in module.rules.
     Until loaders are updated one can use the LoaderOptionsPlugin to pass these options to the loader:
     plugins: [
       new webpack.LoaderOptionsPlugin({
         // test: /\.xxx$/, // may apply this only for some modules
         options: {
           proxy: ...
         }
       })
     ]

今度はwebpack-dev-serverか。

……ああそうか。このfile自体はwebpackのconfigだもんな。serverのconfigとしていれこむならproxyはdevServer.proxyでないといけないのか。

  devServer: {
    contentBase: path.resolve(__dirname, '../pages'),
    port: 3000,
    historyApiFallback: true,
    proxy: {
      '/api/*': 'https://localhost:3000'
    }
  },

これでどうだ。

通った。

ではまずapi serverでjsonが見えるか確認。

% curl http://localhost:3004/test
{
  "name": "test",
  "version": "0.1.0",
  "main": "index.js"
}

結構。では次。proxy経由の確認。http://localhost:3000/api/testで見られるかどうか確認。

% curl http://localhost:3000/api/test
Error occured while trying to proxy to: localhost:3000/api/test

おや駄目だ。logも出ているな。

[HPM] Error occurred while trying to proxy request /api/test from localhost:3000 to https://localhost:3004 (ECONNRESET) (https://nodejs.org/api/errors.html#errors_common_system_errors)

とりあえずここを参考に、/apiが消えるようにする。

webpack.js.org

  devServer: {
    contentBase: path.resolve(__dirname, '../pages'),
    port: 3000,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'https://localhost:3004',
        pathRewrite: {"^/api" : ""}
      }
    }
  }

けれど相変わらず同じerror。

[HPM] Error occurred while trying to proxy request / from localhost:3000 to https://localhost:3004 (ECONNRESET) (https://nodejs.org/api/errors.html#errors_common_system_errors)

ってあ。誰だよhttpsにしてるの。僕だよ。

  devServer: {
    contentBase: path.resolve(__dirname, '../pages'),
    port: 3000,
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: 'http://localhost:3004',
        pathRewrite: {"^/api" : ""}
      }
    }
  }

通った。

% curl http://localhost:3000/api/test
{
  "name": "test",
  "version": "0.1.0",
  "main": "index.js"
}

redux-sagaを使う

さてようやくredux-sagaを使える。 まずはhtmlをいじって、と。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello React</title>
  </head>
  <body>
    <div class="content"></div>
    <hr>
    <div class="content2"></div>
    <hr>
    <div class="router"></div>
    <hr>
    <div class="saga"></div>
    <hr>
    [<a href="./routetest.html">react-router Test</a>]
    <script src="/js/test.js"></script>
  </body>
</html>

roottest.htmlにもdivを追加してあげておく。面倒だけど今はこうしてあげないとrenderの時にdivを見つけられずerrorになるから。 このdiv.sagaのところに描画していく予定。

componentを追加はあとにしてredux-sagaをつなぎ込んでいこう。

/src/route/test.jsxを編集、と。

github.com

これに沿ってちまちまと組み込んでいく。

importは以下を追加。

import { applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

store周りはこのように修正。

const sagaMiddleware = createSagaMiddleware()

const store = createStore(
  combineReducers(reducers),
  initialState,
  compose(
    applyMiddleware(sagaMiddleware),
    (process.env.NODE_ENV !== 'production') ? window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() : undefined
  )
);

/* sagaMiddleware.run(mySaga)*/

本当は最後にsagaMiddleware.runする必要があるけどまだmySaga定義してないから。 これで動作することを確認。

なんかdevtoolsのせいですごく戸惑った。結局middlewareとdevtoolsをcomposeすればいいというのはわかったけど、何故composeすればいいのかはよくわかってない。

github.com

ここにdevtoolsの入れ込み方法が書いてある。でも何故こういう書き方になるのか。

qiita.com

composeは関数の合成。devtoolsの返り値がmiddlewareに入っている、と。なんでさ。

applyMiddlewareはactionがreducerに送られるまでの処理を入れ込む。redux-sagaはactionの派生みたいなものだし納得。

enhancerはwrapしたcreateStoreを返す、と。applyMiddlewareも(dispatch関数を持つ)createStoreを返している。だから結局はcreateStoreを結合しているというだけの話か。

あー。このあたりを理解するにはもう少しcreateStoreの部分というか、Reduxでのaction=>dispatch=>storeの流れをreducerも交えて勉強しないと駄目か。

そして迷走というか勉強へ

hogehuga.com

Middlewareの中にMiddleware、そのMiddlewareの中にMiddleware、、、最後にdispatch。 こうすることで、Redux本来のdispatchが動く前にMiddlewareたちの処理が実行されるわけ。

このあたりの話は先にも出てきたとおり。middlewreというのが何者かと言えば、dispatch(middleware_1(middleware_2(…)))という風になっているもののこと。

ではdispatchとはなんだったか。これはactionをstoreにdispatchする=送る処理。dispatcherはfluxにおいてはsingletonのobjectで、何はともあれcallback経由でstoreにeventを通知するのがお仕事。

で、そのdispatcherをkickするのがaction。だからmapDispatchToPropsにおいてもdispatch(send(value))とかになっている。dispatchを通してsendというactionが生成したeventをstoreに配送している。

ではreducerとは何かと言えば、storeにきたeventに対して何をするかという処理を書いたもの。

だからなんというか、うーん。callbackのせいで複雑なんだよなあ。

とりあえずStoreというものがある。ここは外せない。

Storeに対して働きかける=Storeの値を書き換える方法として、dispatch()がある。

で、どのeventの時、Storeの値の何を書き換えるか、というmethodが登録されているのがReducer。

eventとvalueの対を発行するものがAction。Actionはdispatch()によってeventとvalueをReducerに送る。Reducerはeventに併せてvalueを書き換える。

ああこれ、粒度が微妙に違うんだ。 method levelで考えればActionが実行されDispatcherによってreducerへactionの発行したevent, valueの対が送られ、reducerはeventに対応したStoreのvalueをActionで発行されたvalueに基づいて書き換える。

system architecture levelで考えれば、Actionが実行されることによりDispatcherを通してStoreが更新される、となる。 reducerというのはmethod levelで具体的に値を書き換えるために使われる関数の総称でしかない。

ここまで来ればまあreducerがcontainer(=componentを束ねた一単位のこと)ごとに生成されるのもうなずけるし、どうせStoreはSingletonなのでreducerもそのpageで使う物はすべて一つのreducerに統合されるのも当然。

Storeの定義の多数のcreateStoreの結果をcomposeしているのも、まあ、そりゃあpageに一つのStoreを作って運用するというFlux考え方からすれば当然。

その上でじゃあmiddlewareとは何かと言えば、containerとは独立したところにある、stateの変化によってkickされるactionのことでしかない。だから全部のmiddlewareをたぐって当たりを探しながらvalueを生成して、最後にdispatch関数を投げている。基本的にdomでのeventがactionのtriggerになっているcontainerとは全然違う、Actionのloop。

enhancerはActionとは無関係。単にStoreを拡張する機能。拡張して何をどうするのかは知らないけど。

ただどちらにせよStoreにまつわる処理なのでStoreを作る時にcomposeしているというわけ。

Fluxの概念上の話と実装上の取り回しがこんがらがってひどいことになった。

このように書くと、じゃあdomからのeventでactionが実行されるまでの流れってどうなっているのという気持ちになるけど、そこはmapDispatchToPropsで実装してある。mapDispatchToPropsでeventごとにactionを入れ込んでいく。こんな感じに。

function mapDispatchToProps(dispatch) {
  return {
    onClick(value) {
      dispatch(send(value));
    },
  };
}

で、reduxのconnectという関数を用いて、このactionはどのcomponent(reactで作ったdom)と結びつければいいのか、というのを定義する。

これで全体の流れが再び見えた。

まとめると?

redux-saga周りを使うことを考えて調べた結論として、redux=fluxでいうStoreを実装するためのframeworkだという認識に至る。

react=Viewの管理だけを考えていたとしたら、redux=Storeの管理だけを考えている。

Storeの管理というのはこの場合、Storeの拡張というのもあれば、Storeのdataの書き換えの仕組みの提供というのもある。結果としてActionと、Actionの内容=eventとvalueを送信するDispatcherという考え方も生まれるし、StoreがsingletonだからdispatcherもsingletonでこれらをうまくmapするためのReducerというものが実装上登場してくる。

加えてAction = 普通はViewでのdom操作によるevent発生に紐付いたものだけど、これとは別に、Storeの中のstateが変化することでkickされるActionというものを定義したくなることがあって、それをMiddlewareで対応している。つまり流れは二つあって、

  • A = View(kick event) => Action => (Middleware) => Dispatcher => Store(update state and data) => View(render)
  • B = Store(update state) => Action => (Middleware) => Dispatcher => Store(update state and data) => View(render)

が同時に動く。

で、react-router-reduxはStoreのstateにてurlの変化を検知させる仕組み(store.props.locationに保持している)。

URLの変化をeventとして処理を実行するというのはBの処理。だからこのときAの処理で直接Storeのvalueを読んでrenderに使うのは警告される。だって同時にBの処理が走ってStoreのvalueを書き換えている可能性があるから。なのでcontextという特殊なpropsを使って値の引き渡しをしてね、という面倒な話になってる。こういうvalueの書き変わりのことを副作用という。要約するとつらい。

さてredux-sagaだけど、これはMiddlewareとして実装されている。つまりactionが実行されてDispatcherが実行されるまでの間にいる。

redux-sagaを使うことでActionからDispatcherに至るまでの処理を分岐させ並行処理させることが出来るようになる。つまりtaskのようなものをここで表現できる。

するとどうなるかというと、redux-sagaが提供するRoot-sagaというpipelineの処理が頭から最後まで完了するまで処理を待つことが出来るようになる。つまりAとBの両系統の処理が同時に走ってもredux-sagaの中で足並みそろえて両方終わるまで待つ(必要なものが全部終わってからdispatchする)とかが出来るようになる。

ということだと思う。

www.s-arcana.co.jp

どうもredux-sagaを使うとmiddlewareにいれこむ結果、user requestによるActionもStoreのstateによるActionもすべてredux-sagaに取り込まれることでうまく管理できる状況を作っているらしい。

良い記事だった。

それでどうするよ

Containerを追加してみたらなんだか抽象化したくなってきたのであれこれいじり始めてしまったので続きはまた今度。