Origami
Posted on July 25, 2018
TL;DR
reducerで副作用を起こすと、最悪の場合、コンポーネントのチューニングが不可能になる
本題
こんにちは。まずこのようなコードがありました。
class Account {
constructor(id, name){
this.id = id;
this.name = name;
this.aka = [];
}
pushAka(name){
this.aka.push(name);
return this;
}
}
このインスタンスをreducerで管理したい。そんなことありますよね。たとえば次のように…(redux-actionsを暗黙的に使用している点はご容赦ください)
const initState = new Account('Donald Trump');
export default handleActions({
"ADD_AKA": (state, action) => (state.pushAka(action.payload.aka))
}, initState);
これは、特に何も考えなければ、とりあえずはうまく動きます。しかし、Reduxの3つの基本概念である大前提副作用を起こしてはならないという点において、間違っています。
問題はAccount
クラスのpushAka(name)
メソッドにあります。結局のところ、これは自分自身のメンバ変数を変更した上で自分自身を返しているという事そのものに問題があります。しかし、今の所うまく動きます。
この時点でのStackblitzのサンプルです。
さて、今の所は動いています。すでに大問題ですが、ここから取り返しのつかないことが起きてきます。
Reactは高速ですが、それでもチューニングが必要になる場合が多々あります。コンポーネントの無駄な再レンダリングを防止するために、状況に合わせて主に以下の3つのチューニングが行われます。
-
componentShouldUpdate(prevState, nextState)
の使用 -
React.Component
ではなくReact.PureComponent
を使用 - Stateless Functional Componentsでは
recompose/pure
,recompose/onlyUpdateForKeys
を使う - 自分でpure HoCを書く
さて、この場合でもチューニングを行ってみましょう。今回は先程のサンプルにおいてはcomponents/AkaList.js
がStateless Functional Componentsですから、試しにpure
を使ってコンポーネントのチューニングを行ってみます。単に次のように書き換えるだけです…
import React, {Fragment}from 'react';
import {pure} from 'recompose';
const AkaList = (props) => (
<Fragment>
{props.account.aka.map((v, i) => (<p key={i}>{v}</p>))}
</Fragment>
)
export default pure(AkaList);
recomposeのpure
でコンポーネントの再レンダリングを抑えようとしています(例として少し極端ですが、ご容赦ください。時間がなかったんだ でも動かないサンプルがこちらにある)
すると、リストがあるはずの場所に、何もレンダリングされなくなります。もっと具体的に言うと、コンポーネントがマウントされ、最初のレンダリングがされてからは一切の再レンダリングが行われなくなります。
ある意味では最高のパフォーマンスを得たわけですが、全ての場合においてこれは問題です。
どうするべきなのか
副作用があるような設計が悪いとしか言いようがありません。
ここでは、一番はじめのコードで示したAccount
クラスのpushAka(name)
メソッドが悪いです。そこで、次のようなコードに置き換えます。
class Account {
constructor(name){
this.id = Date.now();
this.name = name;
this.aka = [];
}
pushAka(name){
const r = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
r.aka = [...r.aka, name];
return r;
}
}
r
に自分自身を浅いコピーを行い、その上で新しい配列を作っています。とりあえず、これで動きます。
そしてStackblitzのサンプルです。
なお、この場合はこれでうまく動きましたが、さらに複雑なインスタンス、オブジェクト、配列ではこれだけでは解消しないこともあるでしょう。ただし、そのような複雑なデータ構造はそもそも設計が悪い可能性があります。
Conclution
KEEP PURE FUNCTION, SIMPLE DATA STRUCTURE!
余談
これまでのすべてのStackblitzサンプルにはredux-logger
が導入されています。特に、副作用が起きている時の1個めと2個めで開発ツールを開き、実際にプログラムを動かして、Donald Trumpの二つ名を追加してやりましょう。
何度か試しているうちに、loggerがとても興味深い挙動を記録していることがわかってきます。
なんとprev state
とnext state
が同一のものになっています。それだけではありません。
過去の出力までもが改変されています――とても興味深く、面白い話ですが、この現象を解説するには私はredux
とredux-logger
の実装に精通していません。誰かこの解説記事を書いてください。私はこれが限界です。
Posted on July 25, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.