LoginSignup
15
14

More than 3 years have passed since last update.

ReactでシンプルなDIを行う

Posted at

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

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

$ npm install @mozisan/diact

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

DIコンテナを作る

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()によってDepsProvideruseDepsを得ます。
DepsProviderは実際に依存モジュールを注入するComponentで、useDepsはその注入された依存モジュールを子Componentで利用するためのカスタムHookです。

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

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

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を使います。

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しておきます。

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

export { DepsProvider, useDeps, createLocal };

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

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

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>
  );
};
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を使います。

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も得られ、これを使って次のようにテストを書けます。

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>
  );
};
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におけるモジュール設計の参考になれば幸いです。

15
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
14