DEV Community

Origami
Origami

Posted on

なぜreducerで副作用を起こしてはならないか

TL;DR

reducerで副作用を起こすと、最悪の場合、コンポーネントのチューニングが不可能になる

本題

こんにちは。まずこのようなコードがありました。

class Account {
    constructor(id, name){
        this.id = id;
        this.name = name;
        this.aka = [];
    }

    pushAka(name){
        this.aka.push(name);
        return this;
    }
}
Enter fullscreen mode Exit fullscreen mode

このインスタンスをreducerで管理したい。そんなことありますよね。たとえば次のように…(redux-actionsを暗黙的に使用している点はご容赦ください)

const initState = new Account('Donald Trump');

export default handleActions({
  "ADD_AKA": (state, action) => (state.pushAka(action.payload.aka))
}, initState);

Enter fullscreen mode Exit fullscreen mode

これは、特に何も考えなければ、とりあえずはうまく動きます。しかし、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);
Enter fullscreen mode Exit fullscreen mode

recomposeのpureでコンポーネントの再レンダリングを抑えようとしています(例として少し極端ですが、ご容赦ください。時間がなかったんだ でも動かないサンプルがこちらにある)

すると、リストがあるはずの場所に、何もレンダリングされなくなります。もっと具体的に言うと、コンポーネントがマウントされ、最初のレンダリングがされてからは一切の再レンダリングが行われなくなります。

ある意味では最高のパフォーマンスを得たわけですが、全ての場合においてこれは問題です。

この時点のStackblitzのサンプルです

どうするべきなのか

副作用があるような設計が悪いとしか言いようがありません。

ここでは、一番はじめのコードで示した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;
    }
}
Enter fullscreen mode Exit fullscreen mode

rに自分自身を浅いコピーを行い、その上で新しい配列を作っています。とりあえず、これで動きます。

そしてStackblitzのサンプルです

なお、この場合はこれでうまく動きましたが、さらに複雑なインスタンス、オブジェクト、配列ではこれだけでは解消しないこともあるでしょう。ただし、そのような複雑なデータ構造はそもそも設計が悪い可能性があります。

Conclution

KEEP PURE FUNCTION, SIMPLE DATA STRUCTURE!

余談

これまでのすべてのStackblitzサンプルにはredux-loggerが導入されています。特に、副作用が起きている時の1個め2個めで開発ツールを開き、実際にプログラムを動かして、Donald Trumpの二つ名を追加してやりましょう。

何度か試しているうちに、loggerがとても興味深い挙動を記録していることがわかってきます。

なんとprev statenext stateが同一のものになっています。それだけではありません。

過去の出力までもが改変されています――とても興味深く、面白い話ですが、この現象を解説するには私はreduxredux-loggerの実装に精通していません。誰かこの解説記事を書いてください。私はこれが限界です。

Top comments (0)