30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Enzymeのv3でshallowすると自動的にcomponentDidMountが呼ばれる

Reactのバージョンを16に上げるのに伴いEnzymeのバージョンを3にしたら、一部のテストが壊れた。
調べたところ、v3からshallowの挙動が変わったことが原因だった。
https://github.com/airbnb/enzyme/blob/master/docs/guides/migration-from-2-to-3.md#lifecycle-methods

v2では、明示的に呼び出さない限りcomponentDidMountcomponentDidUpdateは実行されないが、v3ではshallowすると明示しなくても実行される。

v2の挙動

2.9.1で、挙動を確認してみる。

まず、テストの対象となるFooコンポーネントを作成した。

import React from 'react';

export default class Foo extends React.Component {
  constructor() {
    super();
    this.count = 0;
    this.history = [];
  }
  componentWillMount() {
    this.count = this.count + 1;
    this.history.push('componentWillMount');
  }
  componentDidMount() {
    this.count = this.count + 1;
    this.history.push('componentDidMount');
  }
  render() {
    this.count = this.count + 1;
    this.history.push('render');
    return <div>foo</div>;
  }
}

各メソッドが呼ばれる度にcountが増え、呼ばれたメソッドの名前がhistoryに追加されていく。

そのテストコードが以下。

import assert from 'assert';
import React from 'react';
import {shallow} from 'enzyme';

import Foo from '../Foo';

describe('Foo', () => {
  it('Lifecycle', () => {
    const wrapper = shallow(<Foo />);
    assert(wrapper.instance().count === 2);
    assert(wrapper.instance().history.length === 2);
    assert(wrapper.instance().history[0] === 'componentWillMount');
    assert(wrapper.instance().history[1] === 'render');
    wrapper.instance().componentDidMount();
    assert(wrapper.instance().count === 3);
    assert(wrapper.instance().history[2] === 'componentDidMount');
  });
});

shallowした段階では、メソッドが2回呼ばれている。
まずcomponentWillMount、そしてrender
componentDidMountは、明示的に呼び出さない限り実行されない。

v3の挙動

続いて、3.2.0での挙動を見てみる。

ちなみにv3からは、アダプターの設定が必要になる。

// テストのための設定ファイル
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-15';

Enzyme.configure({adapter: new Adapter()});

v3で先程のテストを実行すると失敗する。
次のように書き換えることで、パスするようになる。

describe('Foo', () => {
  it('Lifecycle', () => {
    const wrapper = shallow(<Foo />);
    assert(wrapper.instance().count === 3);
    assert(wrapper.instance().history.length === 3);
    assert(wrapper.instance().history[0] === 'componentWillMount');
    assert(wrapper.instance().history[1] === 'render');
    assert(wrapper.instance().history[2] === 'componentDidMount');
    wrapper.instance().componentDidMount();
    assert(wrapper.instance().count === 4);
    assert(wrapper.instance().history[3] === 'componentDidMount');
  });
});

shallowの時点でcomponentDidMountが呼びされており、wrapper.instance().componentDidMount();とするとさらにもう一度呼ばれていることが分かる。

対策

v2と同じ挙動にしたい場合、shallowを次のように書けばいい。

const wrapper = shallow(<Foo />, {disableLifecycleMethods: true});

テスト全体に適用させたい場合は、テストの設定ファイルなどに{disableLifecycleMethods: true}を書けばいい。
つまり、こうなる。

Enzyme.configure({adapter: new Adapter(), disableLifecycleMethods: true});

Reactのコンポーネントにpropsを渡さない場合のFlowの書き方

Flowのバージョンは0.61.0で確認している。

propsを渡さない場合、Flowはどのように指定すればよいのか

ReactのコンポーネントのpropsにFlowで型をつける場合、以下のように書く。

type Props = {
  foo: string,
};

class ChildComponent extends React.Component<Props> {
  render() {
    return <div>{this.props.foo}</div>;
  }
}
class ParentComponent extends React.Component {
  render() {
    return <ChildComponent foo="hoge" />;
  }
}

https://flow.org/en/docs/react/components/

では、このコンポーネントにはpropsを渡さない場合は、どう書けばいいのか。

propsは、何も渡されなかった場合、空のオブジェクトになる。

class ChildComponent extends React.Component<Props> {
  render() {
    console.log(this.props); // {}
    return <div>foo</div>;
  }
}
class ParentComponent extends React.Component {
  render() {
    return <ChildComponent />;
  }
}

なので、空のオブジェクトを型として設定してみる。

type Props = {};

しかしこれだと、propsを渡してもエラーがでない。
間違ってpropsを渡してしまった場合はエラーを出してくれないと、型として機能していない。

結論を書くと、以下のように定義すればよい。

type Props = {||};

これは、Exact object typesという表記を使って「空のオブジェクト」を定義したものである。

Exact object types

Flowでは通常、指定していないプロパティがオブジェクトに含まれていても、エラーにはならない。
例えば下記の例では、引数として渡したオブジェクトに指定していないプロパティ(boo)が含まれているが、エラーにはならない。

// @flow

function method(obj: {foo: string}) {
  return obj;
}

method({
  foo: '1', // Works!
  boo: 2, // Works!
});

先程のpropsのケースも、これが原因だった。
空のオブジェクトを指定した場合、それはむしろ、あらゆるプロパティを許容することになってしまう。

// @flow

function method(obj: {}) {
  return obj;
}

method({
  foo: '1', // Works!
  boo: 2, // Works!
});

これを防ぎ、厳密にオブジェクトを定義したいときに使うのが、Exact object typesである。
{||}で囲めばよい。

// @flow

function method(obj: {|foo: string|}) {
  return obj;
}

method({
  foo: '1', // Works!
  boo: 2, // Error!
});

そのため、空のオブジェクトは中に何も書かず{||}と定義すればよい。
この場合、空のオブジェクト以外が渡された場合はエラーを出してくれる。

参考資料