📌
テスタブルなReact Componentの書き方
はじめに
最近、JESTとReact Testing Libraryを使ってテストを書く機会が増えてきました。
最初は愚直にテストを書いていたのですが、以下のような課題が出てきたため、コンポーネント構成を見直してみました。
- コンポーネント外のパラメータ(Props や store, API など) と画面表示項目の間に複雑な変換ロジックが入っていてテストを書くのが大変
 - UI の変更などによりテストが通らなくなる
 
Before
1 コンポーネントの中に表示値への変換ロジックが入っているコンポーネントになります。
変換部分が複雑化するとテストを書くのがだんだん億劫になってきます。
Component.tsx
/**
 * Props
 */
export type Props = {
  /**
   * 係数
   */
  coefficient: number;
};
/**
 * Component
 */
export const Component: React.VFC<Props> = ({ coefficient }): JSX.Element => {
  // 押下数
  const [count, setCount] = useState<number>(0);
  // 表示値
  const result = count * coefficient;
  /**
   * 押下時処理
   */
  const handleClick = (): void => {
    setCount((prev: number): number => {
      return prev + 1;
    });
  };
  return (
    <div>
      <div>
        <button onClick={handleClick}>Click Me</button>
      </div>
      <div>{result}</div>
    </div>
  );
};
After
そこでコンポーネントを以下の 3 つに分けてました。
EntryPoint
外部(Props, Store, URL Parameter など)から取得したパラメータを hook に投入し、取得できた View 用の Props を View コンポーネントに渡すだけです。
Component.tsx
import { useHook } from "./Component.hook";
import { View } from "./Component.view";
/**
 * Props
 */
export type Props = {
  /**
   * 係数
   */
  coefficient: number;
};
/**
 * Component
 */
export const Component: React.VFC<Props> = ({ coefficient }): JSX.Element => {
  const viewProps = useHook({ coefficient });
  return <View {...viewProps} />;
};
Hook
入力パラメータから View 用の Props を生成します。
ビジネスロジックは全てここに記載します。
Component.hook.ts
import { useState } from "react";
import type { Props } from "./Component";
import type { Props as ViewProps } from "./Component.view";
/**
 * Params
 */
export type Params = Props;
/**
 * Hook
 */
export const useHook = ({ coefficient }: Params): ViewProps => {
  // 押下数
  const [count, setCount] = useState<number>(0);
  // 表示値
  const result = count * coefficient;
  /**
   * 押下時処理
   */
  const handleClick = (): void => {
    setCount((prev: number): number => {
      return prev + 1;
    });
  };
  return {
    result,
    onClick: handleClick,
  };
};
View
Props で受け取った値を単純に表示するだけです。
Component.view.tsx
/**
 * Props
 */
export type Props = {
  /**
   * 表示値
   */
  result: number;
  /**
   * 押下時処理
   */
  onClick: () => void;
};
/**
 * View
 */
export const View: React.VFC<Props> = ({ result, onClick }): JSX.Element => {
  return (
    <div>
      <div>
        <button onClick={onClick}>Click Me</button>
      </div>
      <div>{result}</div>
    </div>
  );
};
まとめ
ビジネスロジックと View が切り離されることにより、それぞれのテストが書きやすくなると考えています。
また、スピード感が求められるプロジェクトでは Hook だけテストを書くなど、テストを書く範囲を分割しやすいので色々なユースケースに対応できそうです。
Discussion