ReactでシンプルなDIを行う

Reactを使っているとき、Componentにレンダリング以外のロジックを混入させないために、他のモジュールに処理を切り出すということはよく行われると思います。
また、テストの時だけComponentの依存モジュールの実装を差し替えたい、というのもよくあることです。
Reactの`useContext`フックを使えばこれらがシンプルに解決できそうだ、というのがこの記事の趣旨です。

そして自分の中でのユースケースに合わせ、ライブラリを作りました。
基本的にTypeScriptで利用することを想定しています。
[＠mozisan/diact](https://github.com/mozisan/javascript/tree/master/packages/diact)
(とりあえずコードを書いてGitHub上にpushした程度なのでREADMEも書いていませんが…)

```
$ npm install @mozisan/diact
```

以下はこのライブラリを使ってDIを行う方法を紹介します。
(ライブラリのソースを見ればわかる通り、ライブラリでラッピングしている処理はとても単純なので、あえてライブラリを使わず直接`useContext`を使っても同じようなことはできます。)

## DIコンテナを作る

```react
import { createDIContainer } from '@mozisan/diact';

type Foo = {
  readonly doFoo: () => void;
};

type Bar = {
  readonly doBar: () => void;
};

type Deps = {
  readonly foo: Foo;
  readonly bar: Bar;
};

const { DepsProvider, useDeps } = createDIContainer<Deps>();

export { DepsProvider, useDeps };
```

`createDIContainer()`によって`DepsProvider`と`useDeps`を得ます。
`DepsProvider`は実際に依存モジュールを注入するComponentで、`useDeps`はその注入された依存モジュールを子Componentで利用するためのカスタムHookです。

## 依存モジュールを参照する

先ほど得た`useDeps`を使います。

```react
import React from 'react';
import { useDeps } from 'path/to/di-container';

export const App = () => {
  const { foo, bar } = useDeps();

  return (
    <div>
      <button onClick={foo.doFoo}>Do foo</button>
      <button onClick={bar.doBar}>Do bar</button>
    </div>
  );
};
```

## 依存モジュールを注入する

先ほど得た`DepsProvider`を使います。

```react
import React from 'react';
import ReactDOM from 'react-dom';
import { DepsProvider } from 'path/to/di-container';
import { App } from 'path/to/app';

const foo = {
  doFoo: () => {
    console.log('foo');
  },
};

const bar = {
  doBar: () => {
    console.log('bar');
  },
};

ReactDOM.render(
  <DepsProvider deps={{ foo, bar }}>
    <App />
  </DepsProvider>,
  document.getElementById('container'),
);
```

## (Optional) Componentに必要なモジュールだけを参照するようにする

`DepsProvider`は、Component群から参照される全ての依存モジュールを注入しなければいけません。
そのため、`useDeps()`から得られるモジュールの一部は、あるComponentにとって不要なことがあります。
そこで、Componentごとに必要とする依存モジュールを絞り込む機能も紹介します。
(と言っても、これはComponentのテストのために導入した機能であり、これを使うとComponentの実装の観点で何かが便利になるというわけではありません。)

### DIコンテナを作る

実は`createDIContainer()`から、`createLocal`という関数も提供されているので、これを`export`しておきます。

```react
const { DepsProvider, useDeps, createLocal } = createDIContainer<Deps>();

export { DepsProvider, useDeps, createLocal };
```

### 依存モジュールを絞り込んで参照する

先ほど得た`createLocal`を使います。

```react
import React from 'react';
import { createLocal } from 'path/to/di-container';

const { useLocalDeps } = createLocal((deps) => ({ foo: deps.foo }));

export const Foo = () => {
  const { foo } = useLocalDeps();

  return (
    <div>
      <button onClick={foo.doFoo}>Do foo</button>
    </div>
  );
};
```

```react
import React from 'react';
import { createLocal } from 'path/to/di-container';

const { useLocalDeps } = createLocal((deps) => ({ bar: deps.bar }));

export const Bar = () => {
  const { bar } = useLocalDeps();

  return (
    <div>
      <button onClick={bar.doBar}>Do bar</button>
    </div>
  );
};
```

## 依存モジュールを注入する

これは同じく、先ほど得た`DepsProvider`を使います。

```react
import React from 'react';
import ReactDOM from 'react-dom';
import { DepsProvider } from 'path/to/di-container';
import { Foo } from 'path/to/foo';
import { Bar } from 'path/to/bar';

const foo = {
  doFoo: () => {
    console.log('foo');
  },
};

const bar = {
  doBar: () => {
    console.log('bar');
  },
};

ReactDOM.render(
  <DepsProvider deps={{ foo, bar }}>
    <Foo />
    <Bar />
  </DepsProvider>,
  document.getElementById('container'),
);
```

## (Optional) テスト時に、`useLocalDeps()`で参照されるモジュールだけを注入する

前述の通り、`createLocal()`はComponentのテストのために作った機能です。
`createLocal()`の返り値からは`LocalDepsProvider`というComponentも得られ、これを使って次のようにテストを書けます。

```react
import React from 'react';
import { createLocal } from 'path/to/di-container';

const { LocalDepsProvider, useLocalDeps } = createLocal((deps) => ({ foo: deps.foo }));

export { LocalDepsProvider as FooDepsProvider };

export const Foo = () => {
  const { foo } = useLocalDeps();

  return (
    <div>
      <button onClick={foo.doFoo}>Do foo</button>
    </div>
  );
};
```

```react
import { Foo, FooDepsProvider } from 'path/to/foo';

describe('Foo', () => {
  it('works', () => {
    const fooMock = {
      doFoo: () => {
        console.log('foo');
      },
    };

    render(
      <FooDepsProvider deps={{ foo: fooMock }}>
        <Foo />
      </FooDepsProvider>
    );
  });
});
```

テスト時に`DepsProvider`を直接使っても構いませんが、全ての依存モジュールのモックをいちいち注入するのは面倒ですし、テストコードにノイズが増えてしまいます。
`createLocal()`から得られる`LocalDepsProvider`を使うと、必要なモジュールのモックだけ注入すればよいので、テストがシンプルになります。

## おわりに

ライブラリ自体がシンプルなので、使い方もシンプルに収まります。
`createLocal()`を使わなくても結構ですし、なんならComponentごとに`createDIContainer()`によってDIコンテキストを作り、アプリケーション全体を`DepsProvider`で包むことはしないという方法もあるでしょう。
このライブラリを使うとも使わずとも、Reactにおけるモジュール設計の参考になれば幸いです。