React で Dependency injection(依存性注入)

React で Dependency injection(依存性注入)

June 6, 2023

以下2つの記事から、日本語訳で抜粋&整理していきます。

Dependency injection in React - LogRocket Blog #

プロップスで DI #

BEFORE

import { useTrack } from '~/hooks';

function Save() {
  const { track } = useTrack();

  const handleClick = () => {
    console.log('saving...');
    track('saved');
  };

  return <button onClick={handleClick}>Save</button>;
}

AFTER

import { useTracker as _useTrack } from '~/hooks';

interface Props {
  useTrack?: typeof _useTrack;
}

function App({ useTrack = _useTrack }: Props) {
  const { track } = useTrack();

  const handleClick = () => {
    console.log('saving...');
    track('saved');
  };

  return <button onClick={handleClick}>Save</button>;
}

プロップスとの命名の衝突を避けるために _useTrack としていますが、useTrackImpl, useTrackImplementation, useTrackDI なども候補になるかもしれません。

Context API で DI #

多くのライブラリは Context API での DI を提供しています。

import { QueryClient, QueryClientProvider } from 'react-query';
import { useUserQuery } from '~/api';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <User />
    </QueryClientProvider>
  );
}

function User() {
  const { data } = useUserQuery();
  return <p>{JSON.stringify(data)}</p>;
}

Injecting Hooks into React Components | Wolt Careers #

Injecting Component Dependencies #

BEFORE

import React, { useState } from 'react';
import { ComplexChild } from '../components/ComplexChild';
import { Button } from '../components/Button';

interface Props {
  onSomethingHappens: () => void;
}

export const SimpleParent = ({ onSomethingHappens }: Props) => {
  const [isVisible, setIsVisible] = useState(false);
  return (
    <div>
      {isVisible && <ComplexChild onSomethingHappens={onSomethingHappens} />}
      <Button onClick={() => setIsVisible((visible) => !visible)}>Click me</Button>
    </div>
  );
};

AFTER

interface Props {
  ComplexChild: React.ComponentType<{ onSomethingHappens: () => void }>;
  onSomethingHappens: () => void;
}

export const SimpleParent = ({ ComplexChild, onSomethingHappens }: Props) => {
  const [isVisible, setIsVisible] = useState(false);
  return (
    <div>
      {isVisible && <ComplexChild onSomethingHappens={onSomethingHappens} />}
      <Button onClick={() => setIsVisible((visible) => !visible)}>Click me</Button>
    </div>
  );
};

Injecting Hook Dependencies #

React Hooks の利用にあたりいくつか守らなければいけないルールが存在します。コンポーネントは毎回のレンダリングにおいて、同じフックを同じ順番で呼び出さなければいけません。このルールを破ったとき、ランタイムで次のようなエラーに遭遇することになります。

  • Warning: React has detected a change in the order of Hooks called by MyComponent. This will lead to bugs and errors if not fixed.
  • Uncaught Error: Rendered more hooks than during the previous render.
  • Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.

フックを DI する場合、あなたは意図せずこのルールを破りやすくなります。注意してください。

フックを DI する方法は少なくとも3つあります。

  1. Passing Hooks in Props
  2. Currying Hook Parameters (Binding parameters in a function closure)
  3. Passing Hooks Through Context

Passing Hooks in Props #

BEFORE

import React from 'react';
import { useFoo } from 'hooks/useFoo';

export const Child = () => {
  const foo = useFoo();
  return <div>Foo: {foo}</div>;
};

AFTER

import React from 'react';
import { useFoo as defaultUseFoo } from 'hooks/useFoo';

interface Props {
  useFoo?: typeof defaultUseFoo;
}

export const Child = ({ useFoo = defaultUseFoo }: Props) => {
  const foo = useFoo();
  return <div>Foo: {foo}</div>;
};

Currying Hook Parameters #

私はこの手法は使うことがなさそうなので省略。アイデアは面白いのでぜひ元記事を参照することをおすすめします。

Passing Hooks Through Context #

プロップスドリリングがわずらわしいときは Context を使用することもできます。react-facade という小さなライブラリが便利です。

// Assume this is "facade.ts"

import { createFacade } from 'react-facade';

type Hooks = {
  useFoo: () => string;
};

export const [hooks, ImplementationProvider] = createFacade<Hooks>();

コンポーネントはファサードからフックをインポートします。

import React from 'react';
import { hooks } from './facade';

export const Child = () => {
  const foo = hooks.usefoo();
  return <div>Foo: {foo}</div>;
};

親のコンポーネントで ImplementationProvider を利用し、フックの実装を渡します。

import React from 'react';
import { Routes } from 'containers/Routes';
import { useFoo } from 'hooks/useFoo';

// defined in the outer scope to avoid rerenders
const hooks = {
  useFoo,
};

export const App = () => {
  return (
    <ImplementationProvider implementation={hooks}>
      <Routes />
    </ImplementationProvider>
  );
};

終わり #

時間があれば、Wolt Careers の元記事を見てみることをおすすめします。そこそこ長い記事ですが、かなり詳細に書かれていて勉強になるはずです。