LoginSignup
8
5

More than 1 year has passed since last update.

Reactでstorybookとテストを書きやすくする

Posted at

はじめに

Reactで開発していると、storybookやテストの書き方を説明する記事はあります。ただ、日頃どうやったらそれらが書きやすくなるだろうと考えていました。調べたり色々試してみるとと、 Container/Presentational Pattern を使用すると、storybookは書きやすくなり、 カスタムフック でアプリケーションロジックを書くとテストがしやすいと感じるようになりました。
そこで本記事では、それらを使ってどのようにstorybookを書きやすくするか、テストをしやすくしていくかについて残したいと思います。

書くことと書かないこと

  • 書くこと
    • Container/Presentational Patternについて
    • カスタムフックについて
    • 上記二つを使ったロジックの分離の仕方
  • 書かないこと
    • テストの書き方
    • storybookの書き方

Container/Presentational Patternとは

Container/Presentational Patternは

In React, one way to enforce separation of concerns is by using the Container/Presentational pattern. With this pattern, we can separate the view from the application logic.
ref: https://www.patterns.dev/posts/presentational-container-pattern/

とあるように、「関心の分離を矯正する一つの方法」です。ビューからアプリケーションロジックを分離することができます。

カスタムフックとは

カスタムフックは

名前が ”use” で始まり、ほかのフックを呼び出せる JavaScript の関数のこと
ref: https://ja.reactjs.org/docs/hooks-custom.html

です。

複数のコンポーネントで使いまわしたい関数を実装します。

Container/Presentational Patternとカスタムフックを使用しない場合

DogImages.tsx
import React, { useEffect, useState } from "react";

export const DogImages: React.FC = () => {
  const [dogs, setDogs] = useState<string[]>([]);

  useEffect(() => {
    fetch("https://dog.ceo/api/breed/labrador/images/random/6")
      .then((res) => res.json())
      .then(({ message }) => setDogs(message));
  }, []);

  return (
    <>
      {dogs.map((dog, i) => (
        <img src={dog} key={i} alt="Dog" />
      ))}
    </>
  )
};

アプリケーションのロジックと描画のロジックが一つのコンポーネントにまとまっています。
コードが少ない例ですが、これだと

  • ただ単に描画のテストを書きたいのに、データ取得のことまで考慮してテストを書かなければいけない
  • storybookも同様
  • コードが大きくなってきた時に、どこに何が書いてあるのかがわからない
  • データを取得して、加工して、表示して、と一つのコンポーネントの役割が多すぎて、一体どんなテストして良いのかわからない

など、いざstoryやテストを書こうとなった時に以上のような障壁にぶつかるかと思います。ではこれをContainer/Presentational Patternとカスタムフックを使って、紐解いていきましょう。

Container/Presentational Patternとカスタムフックを使用する場合

DogImagesContainer.tsx
import React from "react";
import { DogImagesPresenter } from "./DogImagesPresenter";
import { useFetchDogImages } from "./useFetchDogImages";

export const DogImagesContainer: React.FC = () => {
  const dogs = useFetchDogImages();

  return <DogImagesPresenter dogs={dogs} />;
};
DogImagesPresenter.tsx
import React from "react";

interface P {
  dogs: string[];
}

export const DogImagesPresenter: React.FC<P> = ({ dogs }) => {
  return (
    <>
      {dogs.map((dog, i) => (
        <img src={dog} key={i} alt="Dog" />
      ))}
    </>
  );
};

useFetchDogImages.ts
import { useEffect, useState } from "react";

export const useFetchDogImages = () => {
  const [dogs, setDogs] = useState<string[]>([]);

  useEffect(() => {
    fetch("https://dog.ceo/api/breed/labrador/images/random/6")
      .then((res) => res.json())
      .then(({ message }) => setDogs(message));
  }, []);

  return dogs;
};

DogImagesContainer.tsx がContainer、 DogImagesPresenter.tsx がPresenter、 useFetchDogImages がカスタムフックです。
こうすることで役割が

  • DogImagesContainer.tsx は描画ロジックとアプリケーションロジックをまとめる
  • DogImagesPresenter.tsx は渡された犬の画像URLを元に画像を表示する
  • useFetchDogImages は犬の画像URLを取得して返す

と明確になります。ここまで分かれれば先ほどあげた障壁は解消され

  • 描画のテストを書く際は、ダミーデータを渡して描画のテストを書ける
  • カスタムフックについても個別でテストを書ける
  • storybookはデータ取得ロジックとか気にしなくて良い

という状態にできました。

終わりに

Cons
The Container/Presentational pattern makes it easy to separate application logic from rendering logic. However, Hooks make it possible to achieve the same result without having to use the Container/Presentational pattern, and without having to rewrite a stateless functional component into a class component.Note that today, we don't need to create class components to use state anymore.

Although we can still use the Container/Presentational pattern, even with React Hooks, this pattern can easily be an overkill in smaller sized application.

とあるように、確かに小規模なアプリケーションでは過剰になりがちです。
しかし、業務で扱うアプリケーションは、小規模でないケースがほとんどかと思われます。

小規模でない場合、関心を分離しておけば、チームの誰が見ても(もちろん新しくチームに入ってきた人でも)、どこに何が書いてありそうか、がある程度感覚でわかる、かつテスタブルなコードにできるので、Container/Presentational Patternとカスタムフックを併用する方法はありかなーと個人的に思いました!

少し余談になりますが、画像を取得する

fetch("https://dog.ceo/api/breed/labrador/images/random/6")
  .then((res) => res.json())

に関しては、別途切り出すと、さらにテストが書きやすくなると思います。個人開発で小さなアプリ作る際はそれほど意識する必要ないと思いますが、一定量のコードを書く際は、役割を分けてあげると良いかなと思いました。

多分色々と考慮が足りていない部分もあるはずなので、実際にアプリを作り続けてみてそこを補っていきます。
こうした方が良いよーなどありましたらぜひ :pray:

8
5
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
8
5