💡

【MUI】カスタムpropsがDOMに漏れちゃう?😱 shouldForwardPropsの使い方を調べてみた

に公開

MUIでカスタムコンポーネントを作っている時に独自Propsを作るとエラーが出ました。

独自のpropsをスタイル付けのために渡したら、意図せずHTMLの属性として出力されてしまう現象...。実はこれ、MUI(というか、その内部で使われているCSS-in-JSライブラリ)の「あるある」なんです。

この記事を読めば、その謎をスッキリ解決し、MUIのコンポーネントカスタマイズ方法がわかります。

この記事で学べること:

  • なぜカスタムpropsがDOM要素の属性として出力されちゃうのか?(原因究明編🔍)
  • 救世主shouldForwardPropsって一体何者?(機能紹介編✨)
  • shouldForwardPropsの具体的な使い方(実践編💪)

事件発生!カスタムしたMUIコンポーネントに謎の属性が…!😱

まずは、どんな問題が起きるのか、具体的なコードで見てみましょう。
例えば、MUIのButtonコンポーネントをベースに、アクティブ状態かどうかで背景色が変わるカスタムボタンを作りたいとします。

import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';

// isActive というカスタムpropを持つボタンを定義
const MyCustomButton = styled(Button)(({ isActive }) => ({
  backgroundColor: isActive ? 'salmon' : 'lightgray',
  color: 'white',
  padding: '10px 20px',
  '&:hover': {
    backgroundColor: isActive ? 'red' : 'gray',
  },
}));

function App() {
  return (
    <div>
      <MyCustomButton isActive={true}>アクティブなボタン</MyCustomButton>
      <MyCustomButton isActive={false}>非アクティブなボタン</MyCustomButton>
    </div>
  );
}

export default App;

このコードを実行して、ブラウザの開発者ツールでHTMLを見てみると…

<button class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium css-xxxxxx" tabindex="0" type="button" isactive="true">
  アクティブなボタン
</button>
<button class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium css-yyyyyy" tabindex="0" type="button" isactive="false">
  非アクティブなボタン
</button>

おや?🤔 ボタン要素に isactive="true"isactive="false" という見慣れない属性が付いていますね!
isActiveは、あくまでスタイルを切り替えるために私たちが定義したpropsのはず。HTMLの標準的な<button>要素には、isactiveなんて属性はありません。

これ、何が困るの?って思うかもしれません。
多くの場合、見た目上は問題なく動作するかもしれません。でも…

  • HTMLのバリデーションエラーになる可能性があります。セマンティック的にもちょっと気持ち悪いですよね。
  • Reactが「この属性、知らないんだけど…」とコンソールに警告を出してくることがあります。(例: Warning: Received \true` for a non-boolean attribute `isactive`.`)
  • 将来的に、意図しない副作用を引き起こす可能性もゼロではありません。

やっぱり、余計なものはDOMに出力したくないですよね!

なぜ?犯人はCSS-in-JSライブラリの影響っぽい

この現象、実はMUIが内部で利用しているEmotion(や、styled-componentsといった他のCSS-in-JSライブラリも同様)の基本的な挙動が原因なんです。

これらのライブラリは、コンポーネントに渡されたpropsを、デフォルトではすべてHTML要素の属性として渡そうとします。

「え、なんでそんなおせっかいを…?」と思いますよね。

これには理由があって、例えば<button disabled={true}>のように、HTMLの標準属性(この場合はdisabled)もprops経由で渡しますよね。そのため、基本的には「渡されたものは全部とりあえずDOMに渡してみるか!」という親切設計(?)になっているらしいです。

でも、今回のisActiveのようなカスタムpropは、HTML要素からすると「知らない子」なわけです。
「うーん、便利な機能の裏には、こういうこともあるのね…」と納得しつつも、どうにかしたいですよね!

救世主あらわる!shouldForwardPropsとは?✨

そこで登場するのが、今回の主役shouldForwardPropsです!

shouldForwardPropsは、MUIのstyledユーティリティ(正確にはEmotionの機能)のオプションの一つで、文字通り「どのpropを(DOM要素に)フォワードすべきか(渡すべきか)」をコントロールするための関数です。

この関数を定義してあげることで、私たちは「このpropはスタイル定義にだけ使いたいから、DOMには渡さないでね!」と指示できるようになります。まるで、コンポーネントに渡されるpropsの交通整理をしてくれる、頼れるガードマンさん👮みたいなイメージです。

実践!shouldForwardPropsを使ってみよう💪

では、先ほどのMyCustomButtonshouldForwardPropsを適用して、isActive propがDOMに出力されないようにしてみましょう!

styled関数の第二引数にオプションオブジェクトを渡し、その中にshouldForwardProp(単数形なことに注意!)というキーで関数を指定します。この関数は、prop名を引数に取り、そのpropをDOMに渡すべきならtrue、渡すべきでないならfalseを返すようにします。

import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';

const MyCustomButton = styled(Button, {
  // shouldForwardPropオプションを追加!
  shouldForwardProp: (propName) => propName !== 'isActive', // 'isActive'という名前のpropはフォワードしない
})(({ isActive }) => ({ // スタイル定義関数の中ではisActiveを引き続き使える
  backgroundColor: isActive ? 'salmon' : 'lightgray',
  color: 'white',
  padding: '10px 20px',
  '&:hover': {
    backgroundColor: isActive ? 'red' : 'gray',
  },
}));

function App() {
  return (
    <div>
      <MyCustomButton isActive={true}>アクティブなボタン</MyCustomButton>
      <MyCustomButton isActive={false}>非アクティブなボタン</MyCustomButton>
    </div>
  );
}

export default App;

こうすると、isActiveという名前のpropはDOM要素に渡されなくなります。
実行してHTMLを確認してみると…

<button class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium css-xxxxxx" tabindex="0" type="button">
  アクティブなボタン
</button>
<button class="MuiButtonBase-root MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium MuiButton-root MuiButton-text MuiButton-textPrimary MuiButton-sizeMedium MuiButton-textSizeMedium css-yyyyyy" tabindex="0" type="button">
  非アクティブなボタン
</button>

やりました!🎉 isactive属性が消えて、クリーンなHTMLになりましたね!
shouldForwardProp関数の中では、渡したくないprop名をしっかり指定してあげることがポイントです。複数のカスタムpropがある場合は、それらも同様にフィルタリングロジックに追加してあげましょう。

例えば、myCustomProp1myCustomProp2 をフィルタリングしたい場合は、こんな感じになります。

// ...
shouldForwardProp: (propName) =>
  propName !== 'myCustomProp1' && propName !== 'myCustomProp2',
// ...

これで、意図しない属性がHTMLに紛れ込むのを防ぎつつ、カスタムpropsを使って柔軟なスタイリングができますね!

💡 MUIコンポーネントをラップする場合の注意点

MUIの<Button>のような既存コンポーネントをstyledでラップする場合、colorvariantといったMUIコンポーネント自身が受け付けるpropsは、引き続き正しく渡される必要があります。

通常、MUIのstyledユーティリティは、ラップ対象がMUIコンポーネントかネイティブHTML要素(例: styled('div'))かによって、デフォルトのshouldForwardPropの挙動をある程度賢く制御してくれます。そのため、私たちが主に気にすべきは、「自分で追加した、明らかにHTML標準属性でもMUIコンポーネントのpropsでもないカスタムprop」ということになります。

まとめ:shouldForwardPropsで賢くスタイリング!

今回は、MUIでカスタムコンポーネントを作成する際に出会いがちな「謎の属性問題」と、その解決策であるshouldForwardProps、そしてさらに便利なtransient propsについて解説しました。

  • MUIのstyled(Emotion)は、デフォルトでpropsをDOMに渡そうとする。
  • HTML標準でないカスタムpropsがDOMに渡るのを防ぐには、shouldForwardPropオプションでフィルタリングする。

これらのテクニックを使えば、DOMをクリーンに保ちつつ、MUIコンポーネントのカスタマイズ性を最大限に活かせます。もう、意図しない属性に悩まされることはありませんね!

この記事が、皆さんのMUIライフをちょっとでも快適にするお手伝いができたら嬉しいです😊。
ぜひ、お手元のプロジェクトで試してみてくださいね!そして、もっとMUIの奥深い世界を探求してみてください🚀。

最後まで読んでいただき、ありがとうございました!ハッピーコーディング!✨

Discussion