こんなこともできるstyled-components

## はじめに
[前回の記事](https://qiita.com/course_k/items/2fb57048a95b035d0a5d)では、styled-componentsの超基礎的なところを扱いました。
あまりstyled-componentsについて詳しく知らなかったのですが、ちゃんと調べてみるとかなり使い勝手がいいので、個人的にだいぶ好きになってきています。

本記事では、まだまだ使い所があるstyled-componentsの機能について紹介します。

## 何にでもstyledを適用する
### 味気ないコンポーネント
前回は`styled`でスタイルを適用したコンポーネントを作っていました。

しかし、アプリ開発は自作のコンポーネントだけでは成り立ちません。
例えば、ReactRouterの`Link`コンポーネントを使うことを考えてみます。
※ReactRouterについては[こちらの拙記事](https://qiita.com/course_k/items/09483d4189edce4fbf39)を参考にしてください。

以下のようなコードを定義します。
```react:App.tsx
import { Link } from "react-router-dom";

const App = () => {
  return (
    <>
      <h1>App</h1>
      <Link to="/other">otherへ</Link>
    </>
  );
};

export default App;
```

その他に定義すべきコードは以下の折りたたみから確認してください。

<details><summary>App.tsx以外のソースコードを確認</summary>

```react:main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import Other from "./Other";
import {
  RouteObject,
  RouterProvider,
  createBrowserRouter,
} from "react-router-dom";

const routes: RouteObject[] = [
  {
    path: "/",
    element: <App />,
  },
  {
    path: "/other",
    element: <Other />,
  },
];

const router = createBrowserRouter(routes);
ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);
```

```react:Other.tsx
const Other = () => {
  return <h1>Other</h1>;
};

export default Other;
```

</details>

画面は以下のようになります。
<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/3b1f0208-2021-9903-dfc1-1b4141661e59.png'>

`Link`コンポーネントで生成されるリンクは少し味気ないですね。
もっと自由にスタイルを定義したいとき`styled`を適用できます。


### サードパーティのコンポーネントにスタイルを適用
やりかたはスタイルの継承と一緒です。

```react:App.tsx(スタイル追加)
import { Link } from "react-router-dom";
import styled from "styled-components";

const CustomizeLink = styled(Link)`
  text-decoration: none;
  border: 2px solid gray;
  padding: 5px;
  border-radius: 10px;
  color: black;
  font-size: 20px;
`;

const App = () => {
  return (
    <>
      <h1>App</h1>
      <CustomizeLink to="/other">otherへ</CustomizeLink>
    </>
  );
};

export default App;
```

おしゃれかはわかりませんが、味気ないリンクに装飾を施すことができました。
<img width='70%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/46b68c5c-67bc-8201-0be7-bfe4fb1e1501.png'>

このように、サードパーティのコンポーネントにでも`styled`を使ってスタイルを適用することができます。
もちろん、自作のコンポーネントでも可能です。

### スタイル適用の条件
ただ、実際は何にでもというわけではなく1つだけ条件があります。
それは「propsとして`className`を受け取れる」ということです。

styled-componentsは描画の際に自動でクラスを付与してスタイルを適用しています。
そのため、`className`を設定できる必要があります。

それだけ満たせばどんなコンポーネントにでもスタイルを適用できるため、自由度が高くなります。

## 追加のpropsの定義

### 共通の属性を定義する
外部リンクを扱う場合、セキュリティのためにtarget="_blank"とrel="noopener noreferrer"を設定することが推奨されています。

これらの属性を毎回手動で設定するのは面倒ですし、忘れる可能性もあります。
.attrsを使用することで、これらの属性を自動的に適用できます。
```react
import styled from 'styled-components';
import { Link } from 'react-router-dom';

const ExternalLink = styled.a.attrs({
  target: '_blank',
  rel: 'noopener noreferrer',
})`
  color: #0077cc;
  text-decoration: none;
  &:hover {
    text-decoration: underline;
  }
`;

const StyledRouterLink = styled(Link)`
  color: #0077cc;
  text-decoration: none;
  &:hover {
    text-decoration: underline;
  }
`;

const App = () => {
  return (
    <div>
      <h1>Welcome to my site</h1>
      <ExternalLink href="https://www.example.com">External Link</ExternalLink>
      <br />
      <StyledRouterLink to="/about">About Page</StyledRouterLink>
    </div>
  );
};

export default App;
```
この例では、ExternalLinkコンポーネントを使用するたびに、自動的にtargetとrel属性が設定されます。
また、StyledRouterLinkコンポーネントは内部リンク用のスタイリングを提供します。
これにより、一貫性のあるリンクスタイルを保ちつつ、適切な属性設定を確実に行うことができます。

### 複雑な属性の計算
フォームのバリデーションを考えてみましょう。入力フィールドにエラーがある場合、アクセシビリティのためにaria-invalidとaria-describedby属性を設定する必要があります。これらの属性は動的に変更される可能性が高いため、.attrsを使用して効果的に管理できます。
```react
import React, { useState } from 'react';
import styled from 'styled-components';

const Input = styled.input.attrs<{ $hasError?: boolean }>(props => ({
  'aria-invalid': props.$hasError,
  'aria-describedby': props.$hasError ? `${props.id}-error` : undefined,
}))`
  border: 2px solid ${props => props.$hasError ? 'red' : '#ccc'};
  padding: 8px;
  margin-bottom: 5px;
`;

const ErrorMessage = styled.span`
  color: red;
  font-size: 0.8em;
`;

const FormField = ({ id, label, value, onChange, error }) => {
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <Input
        id={id}
        value={value}
        onChange={onChange}
        $hasError={!!error}
      />
      {error && <ErrorMessage id={`${id}-error`}>{error}</ErrorMessage>}
    </div>
  );
};

const App = () => {
  const [email, setEmail] = useState('');
  const [emailError, setEmailError] = useState('');

  const handleEmailChange = (e) => {
    const value = e.target.value;
    setEmail(value);
    if (!value.includes('@')) {
      setEmailError('有効なメールアドレスを入力してください');
    } else {
      setEmailError('');
    }
  };

  return (
    <div>
      <h1>Contact Form</h1>
      <FormField
        id="email"
        label="Email:"
        value={email}
        onChange={handleEmailChange}
        error={emailError}
      />
    </div>
  );
};

export default App;
```
この例では、Inputコンポーネントに.attrsを使用して、エラー状態に応じてaria-invalidとaria-describedby属性を動的に設定しています。

また、エラーメッセージの表示も制御しています。
これにより、アクセシビリティに配慮しつつ、ユーザーフレンドリーなフォームを簡単に作成できます。

## まとめ
styled-componentsについて、より便利に使うことのできる機能をいくつか紹介しました。
単にスタイルを当てるだけではなく、外部からpropsを動的に変更したり、自由にスタイルを適用したりすることができました。

cssファイルを外部に切り出して、スタイルを定義するよりも汎用性が高く、便利に使えるものなのでは？と感じました。

今後も色々便利なモジュールの使い方を紹介していければと思います。