2025年1月1日

SOLID原則を用いたReactの書き方(発展)

1.Single Responsibility Principle

  • formの入力値の変更やバリデーションなどを自作で書くよりも、React Hook FormやZodなどを用いることで、入力値の管理やバリデーションを簡潔に書くことができる
  • フォームの入力値の変更やバリデーションを自作で書いている場合、コンポーネントが肥大化してしまう。また、バリデーションの変更があった場合、コンポーネント内のバリデーションの変更箇所を探す必要がある。
  • 自作のバリデーションを書いている場合、コンポーネントの分割が難しくなる。一方、React Hook FormのuseFormContextを使用することで、コンポーネントごとに値を管理できるので分割がしやすくなる。

Bad

// フォームの入力値の変更やバリデーションを自作で書いている
function BadComponent() {
  const [user, setUser] = useState({ name: '', age: 0, email: '' });
  const [errors, setErrors] = useState([]);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUser({ ...user, [e.target.name]: e.target.value });

    if (!e.target.value) {
      setErrors([...errors, '入力してください']);
    }
    if (e.target.name === 'age' && e.target.value < 0) {
      setErrors([...errors, '0以上で入力してください']);
    }
    if (e.target.name === 'email' && !e.target.value.includes('@')) {
      setErrors([...errors, 'メールアドレスを入力してください']);
    }
  }

  const handleSubmit = (e: React.FormEvent) => {
    if (errors.length > 0) return;

    // 送信処理
    axios.post('/api/users', user);
  }


  // フォームの表示
  return {...};
}

Good

// zodを用いたバリデーション
const schema = z.object({
  name: z.string(),
  age: z.number(),
  email: z.string().email(),
});

function GoodComponent() {
  // React Hook Formを用いてフォームの入力値の変更やエラーメッセージの管理を行う
  const { register, handleSubmit, formState: { errors } } = useForm<UserFormInput>(
    resolver: zodResolver(schema),
  );
  const onSubmit = (data) => {
    // 送信処理
    axios.post('/api/users', data);
  }

  // フォームの表示
  return {...};
}

2.Open Closed Principle

  • ドロップダウンのメニューを配列の定数で持たせて表示させると拡張性が低い。
  • icon, name, descriptionの内容のみ変更できるのは拡張性が低い。

Bad

function BadComponent() {
  const items = [
    {
      icon: <SignInIcon />,
      name: 'ログイン',
      description: 'すでにアカウントをお持ちの方',
    },
    {
      icon: <SignUpIcon />,
      name: '新規登録',
      description: 'アカウントをお持ちでない方',
    },
  ]

  return (
    <div>
      <Dropdown title="メニュー" items={items} hideIcons={false} />
    </div>
  )
}

function Dropdown({ title, items, hideIcons }) {
  return (
    <div>
      <div>{title}</div>
      {items.map((item) => (
        <div>
          {hideIcons ? null : item.icon}
          <div>{item.name}</div>
          <div>{item.description}</div>
        </div>
      ))}
    </div>
  )
}

Good

function GoodComponent() {
  ;<Dropdown.Dropdown>
    <Dropdown.Button>メニュー</Dropdown.Button>
    <Dropdown.List>
      <Dropdown.Item
        icon={<SignInIcon />}
        description="すでにアカウントをお持ちの方"
      >
        ログイン
      </Dropdown.Item>
      <Dropdown.Item
        icon={<SignUpIcon />}
        description="アカウントをお持ちでない方"
      >
        新規登録
      </Dropdown.Item>
      {/* 以下のようにすぐカスタマイズできる */}
      <span className="px-1 text-xs leading-5 text-gray-400">
        パスワードを忘れた方はこちら
      </span>
    </Dropdown.List>
  </Dropdown.Dropdown>
}

3.Liskov Substitution Principle

  • ダイアログなどのスーパークラスはサブクラスに置き換えやすくあるべき

Bad

// それぞれのダイアログがプライバシーポリシーに同意する処理を持っているため、考慮して改修する必要がある
function PrivacyDialog() {
  const [isOpen, setIsOpen] = useState(false);

  const handleAccept = (id: string) => {
    // プライバシーポリシーに同意する処理
  }

  const openDialog = ("xx") => {
    handleAccept();
    setIsOpen(true);
  }

  const closeDialog = () => {
    setIsOpen(false);
  }

  return (
    <div>
      <button onClick={openDialog}>ダイアログを開く</button>
      <Dialog isOpen={isOpen} onClose={closeDialog}>
        <div>ダイアログの内容</div>
      </Dialog>
    </div>
  );
}

function PrivacyJPDialog() {
  const [isOpen, setIsOpen] = useState(false);

  const handleAccept = (country: string, id: string) => {
    // 国別のプライバシーポリシーに同意する処理
  }

  const openDialog = () => {
    handleAccept("ja", "xxx");
    setIsOpen(true);
  }

  const closeDialog = () => {
    setIsOpen(false);
  }

  return (
    <div>
      <button onClick={openDialog}>ダイアログを開く</button>
      <Dialog isOpen={isOpen} onClose={closeDialog}>
        <div>ダイアログの内容</div>
      </Dialog>
    </div>
  );
}

Good

// どちらのダイアログも同じpropsを受け取るように型定義しているため、一方のダイアログに置き換えやすい
interface PrivacyDialogProps {
  handleAccept: (id: string) => void;
}

function PrivacyDialog({ handleAccept }: PrivacyDialogProps) {
  const [isOpen, setIsOpen] = useState(false);

  const openDialog = () => {
    handleAccept("xx");
    setIsOpen(true);
  }

  ...
}

function PrivacyJPDialog({ handleAccept }: PrivacyDialogProps) {
  const [isOpen, setIsOpen] = useState(false);

  const openDialog = () => {
    handleAccept("xxx");
    setIsOpen(true);
  }

  ...
}

function PrivacyPage() {
  return (
    <>
      <PrivacyDialog handleAccept={handleAccept} /> ←こっちは使わなくなる
      <PrivacyJPDialog handleAccept={handleAccept} /> ←こっちを使う
    </>
  )
}

4.Interface Segregation Principle

  • 通知コンポーネントではproductやユーザー名など複数のドメインによる表示が行われることがある。その場合に、productとユーザーをpropsとして渡すと、不要なpropsを渡してしまうことになる。

Bad

// productとuserをpropsとして渡しているため、片方が存在する場合にもう片方のpropsは不要になる
function BadNotification({ product, user }) {
  if (product) {
    return <div>{product.name}</div>
  } else if (user) {
    return <div>{user.name}</div>
  } else {
    return null
  }
}

Good

// product, userそれぞれの通知コンポーネントを作成することで、不要なpropsを渡さないようにする
function GoodNotification() {
  const { product } = useFetchProduct()
  const { user } = useFetchUser()
  return (
    <>
      {product && <ProductNotification product={product} />}
      {user && <UserNotification user={user} />}
    </>
  )
}

function ProductNotification({ product }) {
  return <div>{product.name}</div>
}

function UserNotification({ user }) {
  return <div>{user.name}</div>
}

5.Dependency Inversion Principle

  • 依存関係を逆転させることで祖結合を確立させる
  • APIを叩く処理およびそのエラーハンドリンングを親コンポーネントから渡して実行することで、apiが変わった際の修正箇所を減らすことができる

Bad

// Formコンポーネント内でAPIを叩く処理を持っている
function Form() {
  ...

  const handleSubmit = async (e) => {
    try {
      e.preventDefault();
      const data = {
        // データ整形
      };

      await axios.post("api/v1/submit", data);
    } catch (err) {
      // エラーハンドリング
    }
  };

  ...
}

Good

// Formコンポーネント内でAPIを叩く処理を持たず、インスタンス化してpropsとして渡す
function Form({ submitService }) {
  ...

  const handleSubmit = async () => {
    await submitService.submitData(formData);
  }

  ...
}

function FormPage() {
  const submitServiceV1 = new SubmitService(
    endpoints.SUBMIT.v1
  );
  const submitServiceV2 = new SubmitService(
    endpoints.SUBMIT.v2
  );

  return <Form submitService={submitServiceV1} />;
}

export class SubmitService {
  constructor(private submitEndpoints) {}

  async submitFeedback(submitData) {
    try {
      const data = {
        // データ整形
      };

      await axios.post(this.submitEndpoints.SUBMIT, data);
    } catch (err) {
      // エラーハンドリング
    }
  }
}

参照

React Clean Code: Advanced Examples of SOLID Principles