2025年1月1日

SOLID原則を用いたReactの書き方

1.Single Responsibility Principle

  • すべてのクラスは1つの責務を持つべきという考え方
  • 適切にコンポーネントを分割する。レンダリングする内容次第ではコンポーネントが肥大化するのを防ぐ
  • データをフェッチ、フェッチしたデータをセットするコードはカスタムフックスに閉じるのが好ましい。(useEffect, useStateを使う)
  • フィルターの処理なども汎用的にできるのであれば関数に切り出す

Bad

// コンポーネントが表示するだけでなく、データをフェッチする処理を持っている
function BadComponent() {
  const [prices, setPrices] = useState([]);

  const fetchPrices = () => {
    fetch(somethingUrl)
      .then((res) => res.json())
      .then((data) => {
        setPrices(data);
      });
  }

  useEffect(() => {
    fetchPrices();
  }, []);

  // 金額一覧を表示
  return {...};
}

Good

// コンポーネントは表示のみを行っている
function GoodComponent {
  const prices = useFetchPrices();

  // 金額一覧を表示
  return {...};
}

// フェッチ処理をカスタムフックスに閉じる
function useFetchPrices() {
  const [prices, setPrices] = useState([]);

  const fetchPrices = () => {
    fetch(somethingUrl)
      .then((res) => res.json())
      .then((data) => {
        setPrices(data);
      });
  }

  useEffect(() => {
    fetchPrices();
  }, []);

  return prices;
}

2.Open Closed Principle

  • 拡張に対しては開いていて、修正に対しては閉じていないといけない
  • 内部で受け取ったpropsに応じて表示を切り替える処理だと、変更箇所について気にしないといけないのは厄介。例えば、roleがmainやbackなどによって表示を切り替える処理を書いていると、roleが増えた場合に変更箇所が増えてしまう。
    • 改善するには、表示自体をpropsで渡すことで表示を切り替える必要がなくなるのでroleが増えたかどうかは気にする必要がない。

Bad

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  text: string
  role: 'main' | 'back' // ここに追加があった場合、変更箇所が増える。可読性も悪い。
}
function Button({ text, role }) {
  return (
    <button {...props}>
      {text}

      {/* roleによって表示を切り替える処理 */}
      {role === 'main' && <MainButtonIcon />}
      {role === 'back' && <BackButtonIcon />}
    </button>
  )
}

Good

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  text: string
  icon: React.ReactNode // 表示するIconをpropsで渡す
}

function Button({ text, icon, ...props }) {
  return (
    <button {...props}>
      {text}

      {icon}
    </button>
  )
}

// 使用する側
function Buttons() {
  return (
    <>
      <Button text="メインボタン" icon={<MainButtonIcon />} />
      <Button text="戻る" icon={<BackButtonIcon />} />
    </>
  )
}

3.Liskov Substitution Principle

  • サブクラスはいつでもそのスーパークラスの代わりとして使用できるべきであるという規範
  • SearchInputコンポーネントはInputHTMLAttributesの型を継承して、コンポーネント内に定義しているinputタグにpropsを展開することでinputタグを継承したSearchInputコンポーネントが出来上がる
  • 参考にしたyoutuberはこのやり方は時と場合によっては当てはまらないかもしれないと言っていた

Bad

// SearchInputコンポーネントがinputタグを継承していない
interface SearchInputProps {
  placeholder: string
  value: string
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
function SearchInput({ placeholder, value, onChange }: SearchInputProps) {
  return (
    <input
      type="text"
      placeholder={placeholder}
      value={value}
      onChange={onChange}
    />
  )
}

Good

// SearchInputコンポーネントがinputタグを継承している
interface SearchInputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

function SearchInput({ ...props }: SearchInputProps) {
  return <input {...props} />
}

4.Interface Segregation Principle

  • インターフェースのクライアントが利用しないプロパティまたはメソッドへの依存を強制してはならないという原則
  • 必要な値のみをpropsとして渡す。オブジェクトの場合は不要なpropsを渡してしまうことがあるため、子コンポーネントが不要なデータと依存関係にあってはいけない。

Bad

// Thumbnailコンポーネントがuserオブジェクトを受け取って不要なデータも渡してしまっている。
interface ThumbnailProps {
  user: {
    name: string
    age: number
    thumbnail: string
  }
}

function Thumbnail({ user }: ThumbnailProps) {
  const { thumbnail } = user
  return <img src={user.thumbnail} />
}

Good

// Thumbnailコンポーネントが必要なデータのみを受け取るようにする
interface ThumbnailProps {
  thumbnailUrl: string
}

function Thumbnail({ thumbnailUrl }: ThumbnailProps) {
  return <img src={thumbnailUrl} />
}

5.Dependency Inversion Principle

  • オブジェクト指向設計の用語であり、ソフトウェアモジュールの疎結合を確立する特別な形態を表現した
  • Formコンポーネント内にSubmit関数がある必要がないので、propsとして渡すことでFormコンポーネントがSubmit関数に依存しないようにする
  • TanstakQueryやSWRなどではデータフェッチをカスタムフックスに閉じることで、コンポーネントがデータフェッチに依存しないようにできる

Bad

// FormコンポーネントがSubmit関数を内部で持っている
function Form() {
  const [user, setUser] = useState({ name: '', age: 0 });

  const handleSubmit = (e: React.FormEvent) => {
    // formの送信処理
    await axios.post('/api/users', { name, age });
  }

  // formの表示
  return {...};
}

Good

// FormコンポーネントがSubmit関数をpropsとして受け取る
interface FormProps {
  onSubmit: (user: { name: string; age: number }) => void;
}

function Form({ onSubmit }: FormProps) {
  const [user, setUser] = useState({ name: '', age: 0 });

  const handleSubmit = (e: React.FormEvent) => {
    onSubmit(user);
  }

  // formの表示
  return {...};
}

// 使用する側
function FormPage() {
  const handleSubmit = (user: { name: string; age: number }) => {
    // formの送信処理
    await axios.post('/api/users', { name, age });
  }

  return <Form onSubmit={handleSubmit} />;
}

参照

This is the Only Right Way to Write React clean-code - SOLID

<!-- SOLIDで有名な記事 -->

The S.O.L.I.D Principles in Pictures