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()
によってDepsProvider
とuseDeps
を得ます。
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におけるモジュール設計の参考になれば幸いです。