Edited at

React/reduxでつくったSPAがリリースされたので学んだことを晒す

More than 3 years have passed since last update.

時系列順に書いているので、話題がアッチコッチいきますが

現場のライブ感を重視しています!

プロジェクトの後半で、すごい優秀な方が入ってきてくれたのでそこからの受け売りも結構混じっています。神様ありがとう。


プロトタイピング

何は無くともまずはプロトタイプを作成しました。

今回はUIライブラリとしてMaterialUIを採用。

superagentを使って外部JSONファイルを読み込んで、Reactコンポーネントとして表示するだけ。

この時点でのコードレビューでの話題は主に、CSSをどうするのか問題。

MaterialUIにコンポーネント自体のstyleは既に定義済みだが、それだけでは足りないレイアウト調整が発生しそうという懸念でした。

結論は、コンポーネント内に直接定義してしまってOK。

実際作業を進めてみたところ、最初の想定よりは補助的なCSS記述は不要でした。


reduxの導入

作成するアプリケーションの規模的に、React単体ですべて構築するのは絶対死ぬと思いました。

公式のサンプルをもとに、簡単な数字をカウントするものをreduxで作成。

react-reduxがどういう仕組みになっているのかがまったく理解できなかったし、とにかくredux自体の学習コストが高かった。

いままでにプログラミング経験が豊富で、デザインパターンを知っている人にとっては大したことないのかもしれませんが、jQueryで簡単なインタラクション制作しかしてこなかったデザイナーにとってはしんどかった!

stateからDOMを構築していくのは非常にわかりやすく、Reactコンポーネント自体の作成は比較的直感的に覚えれたものの、reduxの「どの書き方が正解なのかわからない感」が強かった。

ただ、reduxを詳しく知っていくとJavaScriptの言語仕様の理解が大事だなと感じ、オライリー本が頼りになりました。


react-routerの導入

SPAを作りたかったので、Routerを導入。

自分はサーバーサイドの経験がないので、完全に新境地でやばかった。右も左もわからん。

他のルーティングライブラリがどんなものかあまり見てないけれど、ネストして構造化していくのはHTMLやSassでやってきたことなので、そこはわかりやすかった。

<Router onUpdate={() => window.scrollTo(0, 0)} history={browserHistory}>

この辺の実装はすごい気持ち悪さを覚えたけど…、こういうもん?


Class

React+reduxで構築していくと、自ずとすべて独立した関数ベースで出来ていくのでClassの継承を理解する必要があった。

ES2015で書いていたこともあり、ここはすごくわかりやすかった。

↓最初にコミットした実際のコード

class RequestClass {

constructor() {
this.API_URL = '';
}
get(dispatch, success) {
request
.get(this.API_URL)
.end((err, res) => {
dispatch(success(res.body[0]));
});
}
}
export class RequestUser extends RequestClass {
constructor() {
super();
this.API_URL = '(ここに適当なjsonのパスを書いてた)';
}
}

このタイミングでオブジェクト指向を取り入れようとしたが、作業を進めていくにあたりうまく活かせなかったので途中でやめた。

reduxを使っていることで、必要な値はすべてstoreで包括的に管理されているため、その思想に乗っかってつくるべきと感じました。

頭いい人はたぶん、すごいいい感じになるんだと思った。うんうん。


開発を進めつつ変えていったこと


Fetch API

せっかくなのでリクエストにも新しいものを使おうぜ〜(へらへら)という空気感で、Fetch APIを採用しました。

結論から言うと、コレにしたことで色々なことがありました。(死)


Promise

Fetch API自体は非常に薄いAPIなので、実際にはPromiseを使うのが現実的です。

今更だけどPromise入門

まぁ.then().catch()が書ければ動くっちゃ動く!


クロスドメイン

.ajax()とは違い、デフォルトではクロスドメインへのリクエストはあえて対応されていません。

明示的にオプションで渡してあげる必要があります。

お疲れさまXMLHttpRequest、こんにちはfetch - クロスオリジンとクレデンシャル


polyfill優秀すぎる

whatwg-fetchをつかって、後方互換を一応サポートしています。

本当にimportするだけでサポートされた。神。


ReactComponentのベストプラクティスをひたすら探る毎日

これがまぁ一番難儀したけど、一番やってて楽しいところ。

コードをもとに、変遷を振り返ります。

適当に書き換えたりツギハギしたので微妙に整合性のとれてないコードですがご勘弁を。


初期

class Component extends React.Component {

render() {
const style = {
fontSize: '12px',
color: '#ff528f'
};

let contents = [];
this.props.foo.bar.map((elm) => {
device.push(
<li>{elm}</li>
);
});

return(
<div style={style}>
<Avatar src={this.props.foo.baz} />
<p>{this.props.foo.piyo}</p>
<ul>
{contents}
</ul>
</div>
);
}
}

render()内で終わってます。まぁ最初はこんなもんです。


中期

class Component extends React.Component {

constructor() {
super();
this.handleFoo = this.handleFoo.bind(this);
this.handleBar = this.handleBar.bind(this);

this.style = {
base: {
fontSize: 12,
padding: '8px 24px'
},
button: {
fontSize: 14,
}
};

this.state = {
hoge: [],
fuga: false
count: 20
};
}
componentDidMount() {
if (this.props.foo !== null) {
this.props.actions.requestFoo(this.props.foo);
}
}
componentWillReceiveProps(nextProps) {
this.handleChange(nextProps.data);
}

handleFoo() {
const result = (ごにょごにょ...);
this.handleChange(result);
}

handleChange(value) {
this.setState({
hoge: value
});
}

generateContents() {
let contents = (
list.map((item, i) =>
<div>(ごにょごにょ...)</div>
)
);
return contents;
}

render() {
return(
<div style={this.style.base}>
<div style={this.style.base}>
<RaisedButton
onClick={this.handleChange}
/>
<RaisedButton
onClick={this.handleFoo}
/>
</div>
{this.generateContents}
</div>
);
}
}

イベントハンドラとかが必要になってきて、だんだん構造化してきました。


  • コンストラクタでthisのbind

  • コンストラクタにコンポーネントCSSの定義

  • reduxとは別にローカルStateの定義

  • Reactライフサイクルでイベントの実行

  • DOM構築のメソッドをrenderの外へ切り出し

ここらで扱うstateの数や、そもそもコンポーネントの数などが結構増えてきて

管理が大変になってきます。

(reduxを採用していなかったら、ここで多分死んだ)


後期


container

class Component extends React.Component {

static propTypes = {
foo: React.PropTypes.object.isRequired,
bar: React.PropTypes.array.isRequired
};

static defaultProps = {
foo: {},
bar: []
};

changeBaz = () => {
this.props.actions.changeBaz();
}

get childrenProps() {
const { foo, bar } = this.props;
return {
foo: foo,
bar: bar,
changeBaz: this.changeBaz
};
};

render() {
return(
<Children {...this.childrenProps} />
);
}
};



presentational

const Children = (props) => {

const handleChangeBaz = () => {
props.changeBaz();
};

return (
<div>
<AppInfo
foo={props.foo}
bar={props.bar}
/>
<RaisedButton
onClick={handleChangeBaz}
/>
</div>
);
};

Children.propTypes = {
foo: React.PropTypes.object.isRequired,
bar: React.PropTypes.array.isRequired
changeBaz: React.PropTypes.func.isRequired
};

Children.defaultProps = {
foo: {},
bar: [],
changeBaz: () => {}
};

export default Children;


より管理しやすく、テストしやすくするためにコンポーネントを2種類(Container/Presentational)に分けます。

Containerはreact-reduxにより、storeとconnect()されています。

PresentationalはStatelessFunctionalComponentsとして作られ、親から受け取ったPropsをもとにすべてが完結しています。

Presentational and Container Components

[redux] Presentational / Container componentの分離 - react-redux.connect()のつかいかた

また、babelのpresetで記法を拡張しています。非常に見通しがよくなりました。


.babelrc

"presets": [

"stage-0",
"react",
"es2015"
]

stage-0を入れることで、アローファンクションで記述しておけばthisのbindは明示的に書かなくても大丈夫になりました。

ReactをES6で開発時のbindの問題

(※10/7追記)

stage-0を読み込んでいれば、1や2の指定は不要だそうです。ご指摘ありがとうございました!


redux-thunkからredux-sagaへ移行

明らかなコールバック地獄化していたので、redux-sagaへの移行を決意。

redux-sagaで非同期処理と戦う


大体こんな感じ

function* handleError() {

// 共通のエラー時action
}
export function* handleRequestFoo() {
while (true) {
try {
const action = yield take('ACTION_FOO');
const payload = yield call([Api, Api.login]);
yield put(Actions.success(payload));
} catch(e) {
yield handleError();
}
}
}
export default function* rootSaga() {
yield fork(handleRequestFoo);
...
}


テスト実行時に癒しを求めた

mochaでNyan Cat

思ったよりも精神的に効果あった。


ゴール…!

なんとかリリースしました。

とりあえずキリがついたので、自分の振り返りをかねてまとめてみました。

以下感想。


とにかくreduxを入れて正解だったのと、あとから修正ができやすくなっていることの重要性を身をもって学びました。

UI構築のすべてがコンポーネント基準で作業できるため、少しずつ改善するといった動きも可能だし、一時的な負債もスコープを絞って抱えることができる。

stateを基準にいろいろな判断ができるのも良い。

storeが肥大化してきたらリファクタリングの時期っぽいし、影響範囲の察知もしやすい。

テストコードも切り分けがしやすかったので、とにかくメンテナンス性の高さが目立ちました。

毎日新たな勉強をしながらの作業でしたが、得るものは大きかったです。


おわり!