なんとかredux-sagaを使うに至る話
今回のあらすじ
redux-sagaを使おうとしたけれど理解がおいついていなかった僕はいろいろ調べた結果、これFluxの流れをもう少し理解しないと駄目だわ、となり、いろいろ調べることになって全然実装にたどり着けずに終わった。
今回こそとりあえず使えるようにする。したい。けれどやはり迷走した。したけどなんとかなった、という話。
さてじゃあそろそろ使ってみよう
まずはimport。
import { gettestStore, gettestReducer, GettestContainer } from '/components/GetTest';
そしてrender
ReactDOM.render( <Provider store={store}> <GetTestApp /> </Provider>, document.querySelector(".saga") );
importの中身はReflectFormと同じ。
これでまずは表示できることを確認。とはいえこのままだとGetFormかReflectFormのどちらかをいじれば両方反映されてしまう。修正しないと。
とりあえずGetFormのReducerでのreturnをstate変更なしに変更。
するとReflectFormの方でだけvalueが反映されるようになった。へぇ。そうなるのか……。
えーっとどういじればいいのかな。たぶんeventを変えればそれで通るんだろうけど。
うん。出来た。
eventの名前が重複しないようにどこかに定義した方がいい気がするなあ。あとreducerの名前もか。どうしようかな。
抽象化の始まり
Actionの抽象化 => うまくいかん
とりあえず/components/action.js
にactionの定義を列挙してみた。
Action自体はActionの名前とvalueを返すだけだから外に定義しても特に問題ないはず。でもcomponent間で参照するものでもないんだよなあ。固有の名前になるようにrule決めるか?
action.jsを削除。
actionとreducer周りをこのように定義してみた。
function get(value) { return { type: "GetForm_GET", value, }; } function Reducer(state="", action) { switch (action.type) { case 'GetForm_GET': return Object.assign({}, state, { value: action.value, }); default: return state; } } export const GetFormReducer = {GetForm: Reducer};
この、意図はわからんでもないけど美しくない感がひどい。
うーん。なんだこう。いろいろ定義しなきゃいけないのはわかるんだけどかったるい。classでも用意したらうまくいかないかな。
classでwrapしてみようとしたけどあまりうまくいかない。
一意なeventの名前の付け方とかはまたどこかで考えよう。
あとはたぶんStoreの定義周りがなんかおかしいので修正するか。
Store周りの修正
reduxがsingletonなstoreの管理運用を行うlibraryであるのに対して、containerごとに固有の設定があって、するとcontainerごとにfileを分割するといろんなfileでsingletonなstoreの設定を入れ込むことになって、というのがとても混乱する。
ここをなんとかしたいのだけれどどうしようかな。
いっそreducerの名前などは固定で使うように仕様とするか。
classにしてwrapできれば名前の重複などの場合回避するための方策がとれるけれど、まあよそからcomponentをとってくるようなことも早々ないだろうということで今回は割り切る。
で、createStoreのinitialStateの取り扱いがちょっとわからなくなってきたので見直す。よく見たら今そこにvalueとかいれているけれど、これってなんの効果もないのでは?
ReduxのcombineReducersの仕組みについて理解したいマンという良記事によれば、combineReducersは、その過程でReducerとしての要件を満たしているかを確認する処理assertReducerSanityが呼ばれるという。そこではstateにundefinedを入れて、非nullな値が返るかどうかを検証しているのだ。その検証が通らなければ、combineReducersは失敗する。よって、否が応でも初期状態をReducerに書くことになるのであった。
じゃあまあreducerに初期値書いておけばいいか。今、あえて初期化でいれたいstateってないし。
ああでもreducerの初期値で値をいれた場合は最初のrenderでは反映されないけど、stateとして最初に定義すればrenderに反映されるのか。じゃあ定義はあった方がいいな。ふむ。
redux-sagaに全然入れないけど2つのcontainerを使うことで抽象化するための情報がそろった感じがする。
一度整理。
test.jsxは以下のように修正。
import { namespace as ReflectFormNamespace, State as ReflectFormState, Reducer as ReflectFormReducer, Container as ReflectFormContainer } from '/components/ReflectForm'; import { namespace as GetFormNamespace, State as GetFormState, Reducer as GetFormReducer, Container as GetFormContainer } from '/components/GetForm'; const reducers = { routing: routerReducer, [GetFormNamespace]: GetFormReducer, [ReflectFormNamespace]: ReflectFormReducer }; const initialState = { [GetFormNamespace]: GetFormState, [ReflectFormNamespace]: ReflectFormState };
で、呼び出される方はこうなった。
import { connect } from 'react-redux'; import FormApp from './FormApp'; export const namespace = "getform"; //////////////////////////////////////////////////////////////////////// // Store export let State = { value: null }; function mapStateToProps(state) { return { value: state[namespace].value, }; } //////////////////////////////////////////////////////////////////////// // Action function get(value) { return { type: namespace+"_GET", value, }; } function mapDispatchToProps(dispatch) { return { onClick(value) { dispatch(get(value)); }, }; } //////////////////////////////////////////////////////////////////////// // Reducer export function Reducer(state="", action) { switch (action.type) { case namespace+"_GET": return Object.assign({}, state, { value: action.value, }); default: return state; } } //////////////////////////////////////////////////////////////////////// // Container export const Container = connect( mapStateToProps, mapDispatchToProps )(FormApp);
この、asでaliasするくらいなら何故classで定義しない、と突っ込まれそうなやり口。いやそれすると最後のcoonectができないんや。Containerの定義が出来ないんや……。
そこさえ考えなければまあ割と整理できたんじゃないかと。
なおnamespaceはEventのprefixであり、stateのnamespaceの名前でもある。
classでやると何が面倒ってやはりjavascriptだからかthisの扱いが大変。callbackで呼ばれる関数とかがあるともうthisが全然きかない。
このあたりはもうなとんいうかreducerやstateの登録周りをもう少し賢くなってくれるようreduxに期待するしかないかなあ。containerのfile分割周りをもう少し工夫できないか考える。そろそろredux-sagaに戻りたい。
containerの分割という点では特にどうしようもなさそう。
bundleのrootになるpagename.jsxにbase class作った方がいいかもしれない。
page生成のrootになるjsの抽象化
やってみた。classで分けることが出来た。が、react-routerを使う場合と使わない場合があるので、それはclassを分ける。
classで分けずにparameterでon/offできるようにしても良かったけど、compile後のjsのsizeを削りたいので、必要な場合以外はreact-routerは含まれないようにするため分ける。
結果
redux-sagaはまだ未対応だけどclassの整理は出来た。
まずpage生成のbaseとなる/src/pages/base.jsx
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; import { Provider } from 'react-redux'; import createSagaMiddleware from 'redux-saga' export default class BasePage { constructor(){ this.namespace = []; this.reducers = {}; this.initialState = {}; this.render = {}; } _hasNamespace(namespace){ if(namespace in this.namespace){ // TODO: alert; return 1; }else{ return 0; } } _setComponent(namespace,reducer,state,target,dom){ this.namespace.push(namespace); if(reducer !== undefined) this.reducers[namespace] = reducer; if(state !== undefined) this.initialState[namespace] = state; this.render[namespace] = { dom: dom, bind: target }; } setContainer(namespace,reducer,state,target,dom){ if(this._hasNamespace(namespace)) return 0; this._setComponent(namespace,reducer,state,target,dom); return 1; } setComponent(namespace,target,dom){ if(this._hasNamespace(namespace)) return 0; this._setComponent(namespace,undefined,undefined,target,dom); return 1; } run(){ let sagaMiddleware = createSagaMiddleware() let reducers = this.reducers; let initialState = this.initialState; let 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)*/ for(let name in this.render){ ReactDOM.render( <Provider store={store}> {this.render[name].dom} </Provider>, this.render[name].bind ); } } }
これはこう呼び出される。
import React from 'react'; import { Route } from 'react-router'; import BasePage from '/pages/base'; import HelloApp from '/components/HelloWorld'; import { namespace as ReflectFormNamespace, State as ReflectFormState, Reducer as ReflectFormReducer, Container as ReflectFormContainer } from '/components/ReflectForm'; import { namespace as GetFormNamespace, State as GetFormState, Reducer as GetFormReducer, Container as GetFormContainer } from '/components/GetForm'; let page = new BasePage(); page.setComponent("hello_app",document.querySelector('.content'), <HelloApp />); page.setContainer(ReflectFormNamespace,ReflectFormReducer,ReflectFormState, document.querySelector('.content2'), <ReflectFormContainer /> ); page.setContainer(GetFormNamespace,GetFormReducer,GetFormState, document.querySelector(".saga"), <GetFormContainer /> ); page.run();
file配置はこんな感じ。
./src ├── components │ ├── GetForm │ │ ├── FormApp.jsx │ │ ├── FormDisplay.jsx │ │ ├── FormInput.jsx │ │ └── index.jsx │ ├── HelloWorld │ │ └── index.jsx │ ├── ReflectForm │ │ ├── FormApp.jsx │ │ ├── FormDisplay.jsx │ │ ├── FormInput.jsx │ │ └── index.jsx │ └── RouteTest │ └── index.jsx ├── pages │ ├── base.jsx │ └── routing.jsx └── route └── test.jsx
route以下にあるのがcompileするときのroot file。たぶんそろそろ一から導入しようとする人がいきなり読むにはつらい感じになってきてる気がするけどredux使う限り僕ではこれが今のところ限度。
さてredux-sagaを始めようか。
横道にそれる: Vue.js
横道にそれてvue.jsについてぱらぱらと記事を読んでみる。
たしかにreact使っているとcomponentがどんどん細分化されていくしtemplateとのかみ合わせがよくないのだけれど、Vue.jsならもっと綺麗に書けそう。
あとcomponentが細分化されていくとそれにあわせて書かなきゃいけないcodeも増えてsourceが肥大化するというのも確かになんだか嫌な感じがある。
state管理はいいんだけどなあredux。componentが多すぎるのとjsx=jsにdomも埋め込んだ書き方が気持ち悪いのはなんともはや。
閑話休題
routeとpagesは何かうまくdirを分けられないかなあ。
まあrouteをrootにしてしまおう(安直
さてredux-sagaだ
まずは出力先をpreに差し替えて、this.prop.valueをそのまま埋め込んでみる。こうすればinput formに入力されたものがそこに反映されるようになるはず。
うん、できた。
次はonclock eventとget methodをつなぎ込む処理。
の前にcomponentを整理
一度/src/components/GetForm/FormApp.jsxにすべてまとめてみた。
import React from 'react'; import PropTypes from 'prop-types'; export default class FormApp extends React.Component { get(e) { e.preventDefault(); this.props.onClick(this.myInput.value.trim()); this.myInput.value = ''; return; } render() { return ( <div> Get Form<br /> <form> <input type="text" ref={(ref) => (this.myInput = ref)} defaultValue="" /> <button onClick={(event) => this.get(event)}>Get</button> </form> <pre>{this.props.value}</pre> </div> ); } } FormApp.propTypes = { onClick: PropTypes.func.isRequired, value: PropTypes.string, };
やや紛らわしい。buttonのonClickでthis.get(event)をkickさせて、this.get()の中でthis.props.onClick(this.myInput.value.trim())を実行している。
このthis.props.onClickというのはPropTypes.func.isRequiredで定義されているが、このあたりの動作がいまいちまだイメージできない。
index.jsxの中で
unction get(value) { return { type: namespace+"_GET", value, }; } function mapDispatchToProps(dispatch) { return { onClick(value) { dispatch(get(value)); }, }; }
というのは定義している。だからこれが実行されているのはわかるけど。
あー。もしかしてあれか。mapDispatchToPropsにあるonClickってFormApp classのgetで呼び出しているthis.props.onClickと同じか?
同じだとしてもどうやってつないでる? reduxのconnect使えばmapされるの?
いや違うのか。よく見ればmapDispatchToPropsでreturnしているのは関数……関数なのかな? es6の表記っぽいけど関数のhashだと思う。
で、これをpropertyとmapしているんだから、確かにthis.props.onClickで呼び出せる。FormAppのthis.propsとmapさせるためにreduxのconnect関数を使っているわけだし。
うん流れとしては納得。
あとはこのよくわかってない表記を知りたい。というかclass methodの定義も不思議な感じだったしなあ。es6文法を調べよう。
es6の文法
とりあえずonClickをgetTestJSONに置き換えてみたら動作した。予測通り。
で、es6の文法。
あー。即時実行って{}で囲むだけで良くなったんだ。 thisの値を守るには=>で代入するといい、と。あれ、この間の問題のContainerをclassに落とし込む実装、出来るんじゃない?
arrow functionって実際どういう機能なんだろ。
ES2015(ES6)新構文:アロー関数(Arrow function)|もっこりJavaScript|ANALOGIC(アナロジック)
ES2015の新構文の一つ「アロー関数」とは、無名関数の省略記法です。
ふむ。var fn = function (x) {/* 関数本体 */};
がvar fn = (x) => {/* 関数本体 */};
に置き換わるのか。
でもさっきのpageにあったcharacter => this.name + characterは単に無名関数の省略記法として読むだけではthisが保持される背景としてよくわからない。
アロー関数は無名関数の省略記法ではあるのですが、通常の無名関数と完全に等価というわけではないことに注意しなければなりません。
おっと?
しかし、アロー関数では関数定義時のコンテキスト(スコープ)のthisを常に参照するようになります。言い換えると、アロー関数はそれが定義された場所によって関数内部のthisの値が固定されるということです。
ほほう。納得。
あ、let str = "test value: ${abc}";
ということもできるのか。いいな。
しかし以下のような構文がまだ見当たらない。
{ onClick(value) { dispatch(get(value)); } };
強いて言うならclassが近い。無名のclassとか?
Class構文はまず「class」と書き、その後にクラス名、メソッド定義ブロックと続く。 メソッド定義ブロックは、オブジェクトリテラルでのメソッド短縮定義に似た記法で書く。 ただしメソッドを区切るコンマは置かず、代わりにセミコロンを置いてもよい。
Class構文の振る舞いとしては、まずメソッド定義ブロック中の"constructor"メソッドが特別にクラス名で関数定義され、その関数のprototypeオブジェクトに全てのメソッドが定義される形になる。 つまりclass構文を使っているが、依然「typeof Cat === “function"」であり、あくまで従来型の書き方をスッキリ書ける、糖衣構文に過ぎないという点は注意である。けして新しい概念が入るわけではない。
へえ。なるほど。wrapしているだけなのか。
あーわかったわかった。classとして用意された関数にこれらの関数をつなぐのと同じように、reduxのthis.propsにつなぐ関数一覧の定義として同じ構文を利用した、ということか。
さらに関係ないけどgeneratorなどで使っているfunction* xxx(){}というのも謎。何これpointerなの?
function* 宣言 (末尾にアスタリスクが付いたfunctionキーワード)は、ジェネレーター関数を定義します。
なるほどyield使える関数は特別なのね。
さらに横道にそれてReactDomの並行処理をしようともくろむ
あとReactDOM.renderを今forで回して順次実行させているけどこれ並行処理できないのかな。それともされてるのかな。
いやまあ普通はroot nodeを定義してそこからあとは全部reactで回すという話なんだけど。んー……。
rootはrederingせずにsubtreeを好きなところにrenderingするcomponentが作れるらしい。
es6ならawait asyncが入っているし簡単にできるのではないか。
あ、namespaceのduplication checkがerrorだった。if(namespace in this.namespace)で調べていたけどlistはこの方法じゃ駄目か。
some使えばいいらしいけどcallback書きたくないので=>で定義してthis.namespace.some(x => x == namespace))としてみた。
で、errorをこのように組み込み。
_hasNamespace(namespace){ if(this.namespace.some(x => x == namespace)){ throw new Error(`Failed to add component because "${namespace}" is duplicated namespace.`); return 1; }else{ return 0; } }
いやあ普通に書けるようになってきた。なおtemplate literal使って変数埋め込む場合textはsingle quotationじゃなくて back-ticksで囲む必要がある。
で、これを元に/src/pages/base.jsxを改造してみる。
と思ったけどstore部分をreduxに投げてるんだから別段ここまで複雑にする必要ないよなあ。単に並行してReactDom.renderを実行すればいいのでは。
ためしにこうして一つのcomponentを遅延させてみる。
render() { let x = 1; let y = 0 console.log("A"); for(let i = 0; i<1000000000; i++){ y = x^i; } console.log(y); console.log("B"); return ( <div> Hello React!!<br /> </div> ); }
見事にこれ以降が遅れた。
ということで並行処理する。
といてもまあ単にsetTimeoutでwrapして即時実行させているだけだけど。
for(let name in this.render){ setTimeout(() => { ReactDOM.render( <Provider store={store}> <Router history={history}> {this.render[name].dom} </Router> </Provider>, this.render[name].bind ) } ,0); console.log(name); } }
ただしこれでも止まった。どうも基本的にReactDOM.renderが一つ終わった後にしか進まないらしい。
……いやまあ考えてみればそれもそうか。でないとcomponentの描画がrootからleafへの一方通行にならなくなるし、まだ描画されていないところの描画とかしようとしかねないし。
つまるところ非同期にdom生成をrenderingしてかなきゃいけなくなるような実装するなという話であった。
とするとそうか、各componentを呼び出す初回処理ではそういうものが入っていたら実装として間違っているんだな……。なるほどdata取得を別系統で動かさなきゃいけないわけだ。
あれ、じゃあ初回処理で外からjsonをgetする場合で、それに時間がかかりそうな場合ってどうするんだろう。
単に初回のrenderingが終了したあとでget actionをkickするためのstateを書き換えればいいだけか。たぶん。あるいはredux-sagaでforkさせよう。
redux-sagaに戻る
あーこれ、redux-sagaのために専用のaction定義をする必要があるのか。
するとcontainerごとに定義している現状ではつらいぞ。containerごとにactionもstateも別々に管理しているというのに、これらを統合してあげないといけない。
本来reactでやるrootからleafまでを全部componentでくるんでいくという思想に沿えばこれで別に問題ないのか。
こっちは複数のsubtreeがrootになっているからなあ。んー。
別筋か。APIを叩いてdataを取ってくる処理は完全に別立てで組んで、どこかで統合しよう。
で、とりあえずtest.jsxでSagaを定義しようとしたらerror。
Uncaught ReferenceError: regeneratorRuntime is not defined
npm install --save-dev babel-plugin-transform-runtime
でinstall。
.babelrcを編集
{ "presets": ["react", "es2015"], "plugins": [ ["babel-root-slash-import", { "rootPathSuffix": "src/" }], ["babel-plugin-transform-runtime"], ] }
通った。
で、reducerをいじる。
export function Reducer(state="", action) { switch (action.type) { case namespace+"_GET": return Object.assign({}, state, { value: action.value, }); default: return state; } }
で、redux-sagaを用意。
import { call, put, takeEvery } from 'redux-saga/effects' function* getTestJSON(arg){ yield "username1"; yield "username2"; yield "username3"; } function* fetchJSON(action) { console.log(action); try { const user = yield call(getTestJSON, "arg"); yield put({type: "TEST_FETCH_SUCCEEDED", user: user}); } catch (e) { yield put({type: "TEST_FETCH_FAILED", message: e.message}); } } function* mySaga() { yield takeEvery("TEST_FETCH_REQUESTED", fetchJSON); }
まずここまでを実行。buttonを押したらfetchJSONでactionを受け取ることまでは出来た。
Object {type: "TEST_FETCH_REQUESTED", payload: Object}
ActionのTypeを見てもTEST_FETCH_SUCCEEDEDまで行っている。
redux-sagaとしての動作はしている模様。
んでvalueはどこにいったのか。
……redux-sagaにもreducerを定義してあげる必要があるの? もしかして。
あるっぽいなあ。putはactionをdispatchしているだけらしいし……。
動作の流れはわかったけどどうすればつなぎ込みやすいか少し考える
結局これはStoreをどう管理するかというところに集約される話。 Storeの中に設定するstateと、あとあるComponentが発行するActionの粒度をどのようにそろえるかで全体の構造が変わる。
現状、Actionの粒度に関して言えば、Containerを構成するindex.jsxでReducerを定義することで、それ以下に含まれるすべてのComponentsのActionをそこで包含することができる。 問題はContainerを超えて異なるContainerの持つActionをkickしたくなった時とかだけど、うん、それはDispatcherの動線が大混乱するので、やめよう。割り切りは大事。
問題はStore。redux-sagaで適当な情報を手に入れたとしても、どこにstateとしてstoreするかが謎。
何しろredux-saga自体はStore拡張だからsingletonで動くglobal processみたいなものなのに、Storeの中のどこにどのstateを置くかというのはContainerが決める事柄なので、互いに知るべき情報を知らない状態にある。
ということでStoreをどう管理・構成するのがいいのかというのが目下の課題と言うことになる。
……redux-sagaやめて各Containerのactionの中でajaxを呼び出して結果をdispatchした方がいいような気がするなあ。せいぜい呼び出すajaxだけ再利用できるようにfunctionなりで外部fileに定義しておけばいいだけで。 この場合get/postはいいけどevent sourceはどうしようかという問題がある。現在、event sourceの粒度がpage単位だからなあ。ははは……。
あとこの方式だとStoreと生成されたDomとで情報を二重に持つのでmemory喰いまくるんじゃないだろうか。まあそれはreactの仕様なので僕が考えてもどうしようもないけど。
さてどうするかなあ。
storeの仕様をどこかに書いて管理する? いやあないだろ……。
event sourceをpage単位じゃなくて送りたい情報ごとにわける? 現在の実装だとそれ、server側のprocess数がすごいことになるんだよな……。 そもそも1つのsessionでそんなにたくさんevnet source開けたっけな。開けたにしてもいくつconenction張るんだよって話になるし。
あーそうか。containerをまたいだactionはありえるわ。
うん。それじゃあ仕方ない。Storeを先に設計して、それに沿ってContainerを作り込んでいこう。そういう順番で考えていけばContainerの変更は最小限になるはず。
だからまずはServerに対して1枚のGET/POST/EventSourceなどを叩く関数と、それを決まった場所にStoreする関数を作ろう。
それを前提にContainerはActionを組もう。こうすればStoreに保持されるstateにあわせてmapStateToPropsを作り直すことで、page全体のstateというかdataを管理しているところの変更に会わせてContainerのActionをはっ抗すことも出来るようになるし。
全体構造が変わったじゃねぇかこのぅ。
まあ変わってしまったものはしょうが無い。とりあえず動作確認としてfetchJSONからlocalのserverのapiを叩いてjsonとってこれることまで確認しよう。
今はfetchJSONを叩いてgetTestJSONを実行するところまではできているけれど、generatorとして実装されているからまだよく理解できていないためか、returnが見えないという落ちがついている。 ここをなんとかしよう。
APIのfetchの組み込み
よくみたらapi.jsの例があった。それをまねしてみる。
function* getTestJSON(arg){ return fetch(`http://localhost:3000/api/test`) .then(res => res.json()) .then(payload => { payload }) .catch(error => { error }); } function* fetchJSON(action) { try { const {payload, error} = yield call(getTestJSON, "arg"); console.log(user); yield put({type: "TEST_FETCH_SUCCEEDED", payload: payload, error:error}); } catch (e) { yield put({type: "TEST_FETCH_FAILED", payload:{}, error: e.message}); } }
結果
{ type: 'TEST_FETCH_FAILED', payload: {}, error: 'user is not defined' }
なんかとってこれた。failしてるけど。
そりゃするよ。console.logのせいじゃん。直せ直せ。
{ type: 'TEST_FETCH_SUCCEEDED' }
でもpayloadがない。undefinedだ。
fetch api
server sideのlogを見る限りaccessにはきているなあ。fetch自体は成功している模様。でもdataがとれてない。
そもそもfetchって何かというとまあget/postのapiの一つ。
これにならっていじってみる。
fetch(`http://localhost:3000/api/test`)
.then(res => res.json() )
.then(text => console.log(text));
あ、とってこれてる。じゃあ単にこれを外に出す方法がなんかおかしいんだなー。
return fetch(`http://localhost:3000/api/test`) .then(res => res.json()) .then(payload => { console.log(payload); return payload; }) .catch(error => { error });
こうするとpayloadには狙ったとおりのobjectが入っている。しかしreturnした先で受け取れていない。
{ type: 'TEST_FETCH_FAILED', payload: {}, error: 'Cannot read property \'payload\' of undefined' }
こうするとpayloadはundefinedになっちゃうんだよなあ。なんでさ。
fetch(`http://localhost:3000/api/test`) .then(res => { res.json() }) .then(payload => { console.log(payload); return payload }) .catch(error => { error });
return res.json()
にしたら得られた。
return payload
、return error
にして様子を見てみる。
が、そちらは駄目。
function getTestJSON(arg){ return fetch(`http://localhost:3000/api/test`) .then(res => { return res.json() }) .then(payload => { payload }) .catch(error => { error }); }
とりあえずこれで2つ目のthenでpayloadが認識はされた。で、どうしようか。
余談
しかし思うにStoreを持ち込んでstate管理とstateをtriggerとしたrenderというのが組み込まれた以上、もうこれ、web interfaceだけで完全に別個のapplicationだよなあ……。
単にweb serverからdata取ってくるだけのweb browserで動くapplicationだよなあ……。
そういうつもりで組まないと駄目か。
話を戻す
このようにした場合console.log(test)で結果が返ってきた。
ただしerrorは見えない。
fetchの話とgeneratorの話とyieldのtimingとの話が混ざってよくわからなくなっているなあこれ。
function getTestJSON(arg){ return fetch(`http://localhost:3000/api/test`) .then(res => { return res.json() }) .then(payload => { return payload }) .catch(error => { return error }); } function* fetchJSON(action) { const test = yield call(getTestJSON, "arg"); console.log(test); const {payload, error} = yield call(getTestJSON, "arg"); console.log(payload); console.log(error); if (payload && !error) { yield put({type: "TEST_FETCH_SUCCEEDED", payload: payload}); }else{ yield put({type: "TEST_FETCH_FAILED", payload: {}, error: error}); } }
まずfetchについて。
Promiseベースのため、thenやcatchを使い、処理を書いていく。
それの説明はよく見る。知ってる。
で、Promise自体はcallbackを書かずにthen().then()とつなげていけば同じような実装になるようにしたもの。
今回の場合たぶんfetch()がmainの処理で、そのあとにcallbackしていくものとしてthen()とcatch()をつなげていける。
それはわかるけど、これがcallbackの代替だとすれば、returnを受け取るにはどうしたらいいの? あとcallbackをつなげたときの変数の受け渡し方はどうなってるの?
Promiseはnewして使います。また第一引数に関数を必ず指定をします。関数内では成功時の処理(resolve)と失敗時の処理(reject)を実行する引数が取得できます。
なるほど。もともとresolveとrejectで別系統の処理に分かれるようになっているのか。
thenは第一引数にresolveされたときの処理を、第二引数にrejectされたときの処理をそれぞれ受け取ります。先ほどのサンプルコードでは以下の部分がそれに当たるところです。
ほうほう。だからthen(func1).then(func2)というのは成功した場合だけ連鎖するようになってるのか。
Promise.all([Promise,Promise,…])を使って非同期な処理を並列でおこない、すべてがresolveになるタイミングをthenで取得します。
そういうことも出来るのね。……あれ、前に出来なかったReactDom.render()の並列化これでできるんじゃ?
いや無理だって。ReactDomのrender自体がそんな並列処理しないんだから無理だって。
Promiseオブジェクトには状態があります。それは以下の3種類です。
- pending: 未解決
- fulfilled: 無事完了した
- rejected: 棄却された
ほうほう。
コンストラクタであるPromiseには、newする時、functionを引数として渡します。このfunction内にPromiseがラップしたい処理を書きます。
thenについては?
thenには二つの引数を渡すことができます。第一引数として渡したfunctionは、Promiseオブジェクトの状態がfulfilledになった時、第二引数として渡したfunctionは、Promiseオブジェクトの状態がrejectedになった時に実行されます。thenの第二引数は省略可能です。
このpageも説明が丁寧でいいな。理解できてきた。これを求めていたのだ。
じゃあ改めてfetchに戻って、returnを得る方法を探る。その前にfetch自体の仕様というか使い方をおさらいしないといけないけど、promise baseで組まれているからそのあたりの説明が端折られがちなんだよな……。
Redux DevToolsについて
あ。全然関係ないけど、Storeをwindow.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
でenhanceしているとsafariだと表示できない。
redux devtoolsがそもそもsafari対応してないからだと思う。
fetch周りの書き直し
うーん。基本callback処理で解決するものだからか、fetchの外の変数に値を返すような仕掛けの書き方がない。rejectした場合catchでとってこれるのはわかったけど。
Promiseがどのようにしてコールバック地獄を解消するのに役立つかという話とも似ているのですが、ジェネレータはコードをフラット化するのに役立ちます。つまり、非同期コードを同期的に処理できます。ジェネレータは本質的に、実行を一時停止する関数で、結果的に式の値を返します。
うーん?
とりあえずgeneratorにしてみる。
function* getTestJSON(arg){ return fetch(`http://localhost:3000/api/test`) .then(res => res.json()) .then(payload => payload) .catch(error => error ); }
するとreturnされるのはpromiseになる。
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
pendingだけどいいのかなとおもってbrowserのconsoleで値を展開すると中身が入っている。
とりあえずyieldはgeneratorとして定義された関数の中で処理を止める(その処理が終わるまででblockingする)ものだということはわかった。
するとtestにPromise Classの結果が返ってくるのもわかる。だってfetchの中身がそのまま返ってきてるんだし。特に不思議は無い。
console.log(test)
の時点でtestの中身が変わっているのは、fetchが非同期だから。仮にgetTestJSONでyieldすれば最初から中身が入っていると思う。
入っていたけどreturnされるのがPromise Classでなくなった。
こうなるとあとはgetTestJSONを呼び出しているredux-sagaのcall関数がどういうものかという点にかかってくる。
単に解決方法だけを考えるなら簡単で、errorの時はError classを呼ぼう。fetchJSONではtry-catchで処理すれば別に一つのreturnでも問題ない。
つまりこう。
function getTestJSON(arg){ const url = `http://localhost:3000/api/test` return fetch(url) .then(res => { if(res.ok){ return res.json(); }else{ throw new Error(`Failed to fetch "${res.url}". ${res.status} ${res.statusText}`); } }) .then(payload => payload) .catch(error => {console.log(error); return error;} ); } function* fetchJSON(action) { try { const payload = yield call(getTestJSON, "arg"); yield put({type: "TEST_FETCH_SUCCEEDED", payload: payload, error: ""}); }catch(e){ yield put({type: "TEST_FETCH_FAILED", payload: {}, error: e.message}); } }
が、これだとcatchの中でconsole.log(error)までは見つけられているものの、returnが得られなかった。うぬぬ。
嘘。得られていた。でも正常に取得できているのでthrowしてくれない。
じゃあreturnを常にpayloadとerror両方になるようにすればいいか。
function getTestJSON(arg){ const url = `http://localhost:3000/api/test` return fetch(url) .then(res => { if(res.ok){ return res.json(); }else{ throw new Error(`Failed to fetch "${res.url}". ${res.status} ${res.statusText}`); } }) .then(payload => ([payload, undefined])) .catch(error => ([undefined, error])); } function* fetchJSON(action) { const [payload,error] = yield call(getTestJSON, "arg"); if(payload && error === undefined){ yield put({type: "TEST_FETCH_SUCCEEDED", payload: payload, error: error}); }else{ yield put({type: "TEST_FETCH_FAILED", payload: payload, error: error}); } }
これで返ってきた。
最後に画面を調整
次はbuttonによって成功するapiと失敗するapiを叩いて、値の変化が適切に保存されるかを確認する。
成功。無事書き換わっている。
取得したdataをprintしてみる。
できた。
一区切りかー。しかしこれsourceが長くなるなあ。Vue.jsに流れたくなるわけもわかるわ。わかればすごく頑健に組めるけど手軽じゃない。C++か。
code量が増えるのと、Storeの見通しが良くなるように全体構造を整えなきゃいけないのが課題。実装上の規則が増えそうで嫌な感じはある。
次はそこを考えよう。ここがまとまると、開発環境と構築方針が固まって以後実装に専念できる。
Github
ということでv0.1.1にしてここまでを保存。次で環境整って一区切りかなぁ。