見出し画像

Next.js + MUIのスタイル再定義とアトミックデザインの話

こんにちは。やっくんです。
Webデザインとかフロントエンド周りを触ったり触らなかったりしている者です。

自分の頭の整理と発信の練習も兼ねて、マイペースになると思いますがnoteを更新していきます。

この記事では『MUIの機能は引き継ぎつつ、スタイルは自分のプロジェクト用に再定義して運用していく方法』について書いていきたいと思います。
あとそれに伴った『UIライブラリ+アトミックデザインの運用方法』についても語ってみます。

UIライブラリやデザインシステムは多くの方が悩み苦しみ、頭を抱えているのでは…と思い、自分なりにはなりますがひとつの考えを置いておきます。
どなたかのヒントになれば幸いです。

ちなみに僕は頭を抱えすぎて逆に頭に抱えられました。

記事の趣旨

この記事はこんな方におすすめ

  • MUIをカスタマイズしたい!

  • 中〜大規模プロジェクトにおけるコンポーネント設計に悩んでいる…

  • UIライブラリ+アトミックデザインって結局どう運用するんよ

こんな方にはあんまりかも

  • MUI以外のUIライブラリを検討したい

  • Reactの基礎を学びたい

  • 状態管理、テストツールについて悩んでいる

  • 幸せの定義を探している

今回はUI管理がテーマなので、状態管理やテストについては触れません。

また、MUIとReactの基礎を理解していることを前提に進めますのでご了承ください。

とはいいつつも各種おさらい

Reactとは

元Facebook社が開発したUIを構築するためのJavaScriptライブラリ。
宣言的であることとコンポーネント思想が特徴です。
SPAでない限りフレームワークとしてNext.jsを採用することが多いと思います。


MUIとは

GoogleのMaterial Designをベースに開発されたUIコンポーネントライブラリです。
もともとはMaterial Designという名前でしたが、v5からMUIになりました。


アトミックデザインとは

5つの大小ある階層に分類されたUIコンポーネントで全体のUIを構築していくデザインシステムのひとつです。

Atoms < Molecules < Organisms < Templates < Pages

の階層からなり、右にいくにつれ単位が大きくなります。

Atoms: これ以上分割できない最小単位のコンポーネント
Molecules: Atomsから成る単一機能を持ったコンポーネント
Organisms: Atoms, Moleculesから成る複合的な機能を持ったコンポーネント
Templates: Atoms, Molecules, Organismsからなるドメインの情報を持たないページの雛形のコンポーネント
Pages: Templatesに情報を載せた、ブラウザに描画されるコンポーネント

おもにこういった分類があります。
ただあくまでも設計思想であり、アトミックデザイン自体はフレームワークではありません。

なので独自のルールを追加するなどで臨機応変に拡張し、要件に合わせて使いやすいようチームと方針を決めていくものだと思っています。
(そもそもMoleculesとかよくわからんし、草<木<林<森 とかのほうがわかりやすいという人もいます)

①ディレクトリ構成どうする問題

まず悩むのがここで、MUIを適宜importするとなるとアトミックデザインの意味を成さないので、少しの工夫が必要です。

結論、あくまで一例ですが下図のような構成はいかがでしょうか。

フロント簡易構成図

※実線矢印: import
※点線: 補足
※水色付箋: 今回のテーマに大きく関わる補足
※黄色付箋: プチ補足
※こういうの使う: こういうの使う

アトミックデザイン + αな考え

まず、/src/components配下でアトミックデザインに則ったディレクトリを切ります。

ただPagesにおいては、Next.jsにおけるpagesとしました。
ここでは/components/Templates配下のコンポーネントを呼び出し、そこにドメインに関する情報を渡す役割を担います。

なぜそんなことをするのか?
この構成は以下のようなパターンで非常に役に立ちます。

  • https://{hoge}/top

  • https://{fuga}/top

  • https://{piyo}/top

のように同一プロジェクト内に複数のサービスがあり、ページの構成はまったく同じだけど見せたい情報が違う、などといった場合です。

このときpages配下にはhoge/top.tsx, fuga/top.tsx, piyo/top.tsxがあるわけですが、それぞれのファイルで同一コンポーネントであるcomponents/Templates/TopTemp.tsxをimportします。

Templates配下のコンポーネントはドメインの知識を持たない枠にしかすぎないため、hoge, fuga, piyoではそれぞれのドメインに紐づく情報をTopTemp.tsxに渡すだけ、ということになります。

もし改修が走る場合はTemplatesに該当する一ファイルのみの改修ですべてのページに反映されるため、運用保守の面でメリットが大きいです(そもそもコンポーネント思想ってそういうものですが)。

このパターン以外にも、Templateがレイアウト的な役割を持っており、複数のPageコンポーネントで使われる場合もこの構成がよいと思います。

反対に、TemplateとPageが完全に一対一の関係である場合はTemplate = 実質Pageになってもよいかなと思います。


また、本来のアトミックデザイン的思想では自身と同階層にあたるコンポーネントをラップすることはできません。

ただ要件によってはこのルールがあると実質的に実現不可能なケースが出てくることもあるかと思います。
実際に私もその状況になったり、本来のルールでは不便なことも見えてきたため、以下のような追加ルールを設けてチームで共通認識をもちました。

  • 各階層配下は適宜ディレクトリを切ってもよい

  • Molecules, Organismsに限り自身をラップすることを許容する

  • Templates配下のコンポーネントは{hoge}Temp.tsxのように末尾にTempをつけ命名する

おもにこの三点です。

一点目は管理の問題で、一階層にあまりに多くのファイルが存在していると単純に邪魔です。
具体的にはAtoms/Iconsなどのディレクトリを設け、その中にアイコンのコンポーネントを入れ管理する、などといったことを表します。

二点目はそのままです。
自身をラップしないと実現できない状況が多々ありました。
ディレクトリをオリジナルで増やすという選択肢もなくはないですが、最終的に/Planets, /Galaxiesなどになる可能性もあり、さすがに壮大すぎるので辞めました。

三点目は文法的な問題で、最終的にPages配下で呼び出されるテンプレートファイルはpages配下のコンポーネントと名前が被ってしまうため、末尾にTempとつけるようにしました。
ここは要件次第で変化しやすい部分だと思います。

はい、以上で基本的なディレクトリ構成についてはおわりです。

②MUIのスタイルを上書きして再定義する

ようやくメインテーマです。
さっきの図の②の部分です。

フロント簡易構成図(2回目)

とりあえず結論

結論、こんな感じにしたらいいかなと思ってます。

import { Button as MuiButton, ButtonProps, styled } from "@mui/material";

interface StyledButtonProps extends ButtonProps {
  isRound: boolean;
}

export const Button = styled(MuiButton)<StyledButtonProps>(
  ({ isRound, theme }) => ({
    backgroundColor: theme.palette.success.main,
    borderRadius: isRound ? "100vw" : "10px",
  })
);

順に見ていきます。

import { Button as MuiButton, ButtonProps, styled } from "@mui/material";
// 省略

MUIにおけるButtonをMuiButtonという名前で上書きしてimportします。
あとついでにButtonの型情報であるButtonPropsと、スタイル定義に使うstyledも入れておきます。

// 省略
interface StyledButtonProps extends ButtonProps {
  isRound: boolean;
}
// 省略

interfaceを使い、extendsでButtonPropsを継承したStyledButtonPropsを新たに作成しました。
こうすることで元々のButtonPropsにあるPropsに加え、オリジナルで定義したisRoundを使用することができるようになります。

// 省略
export const Button = styled(MuiButton)<StyledButtonProps>(
  ({ isRound, theme }) => ({
    backgroundColor: theme.palette.success.main,
    borderRadius: isRound ? "100vw" : "10px",
  })
);

exportしたいコンポーネント名をButtonにするため、あらかじめMuiButtonと名前付きimportにしておきました。
styledの引数としてMuiButtonを渡し、型は先ほど定義したStyledButtonPropsを指定します。

({ isRound, theme })の部分では自作のPropsとMUIのthemeを渡しています。

アロー関数の先ではCSSをオブジェクトで指定します。
backgroundColorの値にはMUIのテーマにあるサクセスのメインカラーにしてみました。
bourderRadiusの値は先ほど追加したisRoundをtrue, falseで判定し、trueであれば100vw, そうでなければ10pxが指定されるよう条件分岐にしてみました。

では早速、スタイルを再定義したこのButtonコンポーネントをブラウザ上に描画してみます。
pages/sample.tsxを作成し、以下のようにしてみました。

import { Button } from "../components/Button";

export default function Sample() {
  return (
    <>
      <Button variant="contained" isRound={true}>
        サンプル丸ボタンです
      </Button>
      <Button variant="contained" isRound={false}>
        サンプルちょい丸ボタンです
      </Button>
    </>
  );
}

ただimportして並べただけですが、MUIの機能を引き継いでいることを確認するためにvariant="contained"を指定しました。
これはMUIのButtonに備わっている機能で、ボタンの塗りや線をPropsで指定することができます。

また、先ほど追加したisRoundがtrueとfalseのパターンを置いてみました。
ブラウザで見るとこうなってます。

MUIのデフォルトだと青いボタンですが、再定義したため使う側で指定せずとも緑色になっていることがわかります。
また、isRoundの値によりCSSが動的に切り替わっていることが確認できました。

文字の色が勝手に白くなっているのはvariant="contained"を指定したためで、MUI側の初期設定により塗りつぶしボタンに対しては文字が白くなります。

このように、再定義の場面では型の追加とスタイルの上書きしかしていないため、ホバー時の挙動やその他の機能的な部分はすべて引き継がれています。

MUIコンポーネントを使わない場合は?

ときには自作コンポーネントを定義したいときだってあります。
そんなときもThemeからカラーを参照したい。みなさんそう思いますよね。

styled()はユーティリティとしてコンポーネント群からは切り離された機能であるため、自作のコンポーネントにも利用できます。

例として、MyErrorBoxというコンポーネントを定義してみます。
HTML要素としてはdivを使い、MUIコンポーネントは使用せずstyled()のみ利用します。

import { styled } from "@mui/material";

export const MyErrorBox = styled("div")(({ theme }) => ({
  color: "#fff",
  backgroundColor: theme.palette.error.main,
  borderRadius: "10px",
  padding: 8,
}));

styled()に"div"を渡し、あとは先ほどと同様にthemeも渡してCSSを書きます。
backgroundColorの値にtheme内の色を指定していることがわかります。

定義したMyErrorBoxコンポーネントを呼び出すとき、たとえば下記のようになります。

import { MyErrorBox } from "../components/MyErrorBox";

export default function Error() {
  return <MyErrorBox>エラーが発生しました。</MyErrorBox>;
}

<MyErrorBox>で「エラーが発生しました。」というテキストをラップしてみました。
ブラウザで見るとこうなっています。

backgroundColorとしてtheme.palette.error.mainが適応されていることが無事に確認できました。

以上のことを踏まえると

  • MUIの機能は引き継ぎつつ、スタイルだけを上書きして自分の環境で再定義

  • MUIコンポーネントを使用しない場合も同じThemeから色を参照する

のふたつが叶います。
あとはこれらを駆使してアトミックデザインの思想に則り設計、UI実装を行うのみ!

長くなりましたが「Next.js + MUIのスタイル再定義とコンポーネントシステムの話」はここまでとなります。

読んでいただきありがとうございました。

いや…Themeも拡張したいんだけど…

はい、画像にあった③の部分ですね。
カスタマイズ方法はわかったとて、ThemeにあるカラーがMUIの初期装備だけじゃやっていけねえじゃん、ということですね。

フロント簡易構成図(3回目)

ただ、ここまで書くとさらに長く長くなってしまうので、さすがに別の記事にわけようと思います。

完成したらまたリンクを貼りますのでそれまでお待ちを。

では、以上MUIのカスタマイズとデザインシステムに関するお話でした〜。


この記事が気に入ったらサポートをしてみませんか?