3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ReactとMaterial-UIのTIPS集

Posted at

この記事について

ここ最近React + MUIでフロントエンド開発をしており、自分の中でベストプラクティス的なものができつつあるので一旦まとめようと思い書いた記事
一部調査中につきTODOの状態(調査完了したら追記する予定)

React関連

コンポーネント

関数コンポーネントを使おう

Reactのコンポーネントは関数コンポーネントクラスコンポーネントの2つに大別される
公式ドキュメントから引用

// 関数コンポーネント👍
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// クラスコンポーネント
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

どちらも全く等価なのでどう書くかは好みの問題になるが、個人的には記述量の少ない関数コンポーネントの方が良いと思う
また、関数コンポーネントはさらに以下の2通りの記法があり

import React from "react";

type Props = {
  name: string;
}

// 関数式👍
function Welcome(props: Props) {
  return <h1>Hello, {props.name}</h1>;
}

// アロー関数
const Welcome: React.FC<Props> = (props: Props) => {
  return <h1>Hello, {props.name}</h1>;
};

TypeScriptで型定義をする場合はより記述量の少ない関数式(function)の方が望ましいと思う
※ぶっちゃけ関数コンポーネントは戻り値がJSX.Elementであればいいのでアロー関数でわざわざReact.FCで型付けする必要もそんなにないし、その場合記述量の差はほぼないので好みで決めていいと思う

AtomicDesignに従って階層を分けよう

ReactはUIの部品をコンポーネントという単位で宣言して再利用可能にするのがウリ(と自分は思っている)なので、それを活かるようなディレクトリ構成にしましょうという話
AtomicDesignそのものについてはわかりやすい記事がたくさんあるので適当に見繕って読んでいただくとして、以下ではAtomicDesignをReactでのコンポーネント開発のケースにあてはめて解説する

Atoms

AtomicDesignの最小単位
AtomicDeisgnの原典では以下のように定義されている

These atoms include basic HTML elements like form labels, inputs, buttons, and others that can’t be broken down any further without ceasing to be functional.

つまり、<input><button>などのHTMLタグをそのままAtomsのコンポーネントとして実装すればOK
また、そうでなくても(複数のタグを組み合わせて実装しても)それ以上分解できないほどそのコンポーネントの役割が抽象的なのであればAtomsコンポーネントとして実装することになる

なお、Atomsの実装で重要なのはwidthやpaddingなどを決め打ちしないことと、それらを外部から注入できる形にする(=propsで受け取れるようにする)こと
Atomsのコンポーネントはあちこちで使いまわすことになるので、サイズを決め打ちしてしまうとSmallButtonとかNormalButtonみたいなサイズだけ違うボタンが乱立したりCSSで強引にサイズを変更したりとカオスなコードになってしまう

一方でなるべくデザインに統一感を持たせるためdefaultValueとしてwidthやpaddingを設定するのは大変有効で、他にもsmall=200px, normal=300px...のようなenum的な値を作っておくのも良いと思う

Molecules

Atomsを組み合わせて実装するコンポーネント
やや大きめの粒度のコンポーネントを実装する際それがMoleculesなのかOrganismsなのかよく悩んでいたが、自分は以下のようなフローチャートにのっとって決定するようにしている

単体のHTMLタグそのものか、そのコンポーネントの役割が極めて抽象的 ─YES→ Atoms
 │
 NO
 ↓
そのコンポーネントの役割がビジネスロジックと結びつくもの、具体的なリソースと直接関わるものである ─YES→ Organisms
 │
 NO
 ↓
Molecules

具体例を挙げると(Atomsは自明なのでスキップ)以下の通り

  • Molecules
    • 検索フォーム
      • 役割は検索したい文字列を入力し、検索機能を発火することであって、具体的に何を検索するかは定められていない(ビジネスロジックと結びついていない)のでMolecules
    • 設定ボタン
      • こういうやつ→⚙️
      • これも何らかの設定画面ないしダイアログを開く役割を持つものの、具体的に何の設定かは定められていないのでMolecules
  • Organisms
    • ユーザー検索フォーム
      • 役割がユーザーの一覧取得APIをリクエストすることで、ビジネスロジックと結びついているのでOrganisms
    • ユーザー設定ボタン
      • 役割がユーザーの設定変更画面を開くことなのでOrganisms

Organisms

AtomsやMoleculesを組み合わせて実装する
Organisms内の各コンポーネントのスタイリングはしてもいいがOrganisms自体のサイズはここで決め打ちしてはいけない

Templates

Pagesから呼び出されるコンポーネントで、渡されたコンポーネント(基本Organismsのみ)をどのように配置するかについてのみ責務を負う
→スタイリングはするけどロジックはここでは持たない

Pages

TemplatesへOrganismsを渡す
また、自身の子コンポーネント(=Organisms)へ渡すべきstatesや関数などはここで宣言する

各コンポーネントの依存関係と役割のまとめ

図にまとめると以下のようになる
dataflow.png

  • Pages
    • Organismsを呼び出しTemplatesに渡す
    • 基本的にMolecules、Atomsには関心を持たない
  • Templates
    • Pagesから受け取ったコンポーネント(=Organisms)を表示するだけ
    • 各コンポーネントの大きさや配置などはここで決定する
  • Organisms
    • Atoms、Molecules、必要なら他のOrganismsを呼び出して1つのコンポーネントを作る
    • 基本的に責務は一つでドメインと結びつくもの
      • チャットを送信するフォーム、商品一覧のページング、プロフィール画面へ遷移するボタンなど
  • Molecules
    • Atomsを組み合わせて実装する
    • Organismsと異なりドメインとは直接結びつかない抽象的なコンポーネント
      • テキストボックスを複数持つ何らかの入力フォーム、ページ番号のstateを持つ汎用的なページングなど
  • Atoms
    • HTMLのタグと1対1で対応するコンポーネント

カスタムフックの実装

カスタムフックでviewとロジックを分離しよう

例えば以下のようなログインフォームがあった場合

import React from "react";

export function LoginForm() {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        AuthAPI.login(email, password)
      }}
    >
      <input type="email" value={email} onChange={(event) => setEmail(event.target.value)} />
      <input type="password" value={password} onChange={(event) => setPassword(event.target.value)} />
      <button type="submit">LOGIN</button>
    </form>
  );
}

最低限のロジックしかなくCSSすら書いていないのに結構煩雑で読むのが面倒くさい
また、UTを書く際にもviewとロジックを1ファイルにまとめて書くことになるのでメンテナンスが大変

なので、以下のようにあるコンポーネントで使用したい変数、関数をまとめて返すようなカスタムフックを実装してviewとロジックを分離しよう

useLoginForm.ts
import React from "react";
import { LoginFormProps } from "./LoginForm.tsx";

export function useLoginForm(): LoginFormProps {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  const onChangeEmail = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) =>
      setEmail(event.target.value),
    []
  );
  const onChangePassword = React.useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) =>
      setPassword(event.target.value),
    []
  );
  const onSubmit = React.useCallback(
    (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      AuthAPI.login(email, password);
    },
    [email, password]
  );

  return {
    email,
    onChangeEmail,
    password,
    onChangePassword,
    onSubmit,
  };
}
LoginForm.tsx
import React from "react";

export type LoginFormProps = {
  email: string;
  onChangeEmail: (event: React.ChangeEvent<HTMLInputElement>) => void;
  password: string;
  onChangePassword: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
};

export function LoginForm(props: LoginFormProps) {
  return (
    <form onSubmit={(event) => props.onSubmit(event)}>
      <input
        type="email"
        value={props.email}
        onChange={(event) => props.onChangeEmail(event)}
      />
      <input
        type="password"
        value={props.password}
        onChange={(event) => props.onChangePassword(event)}
      />
      <button type="submit">LOGIN</button>
    </form>
  );
}

色んなページから参照したい値はContextAPIで管理しよう

例えばAPI実行中に二重でサブミットするのを防ぐためボタンをdisabledにするためのフラグや、認証したユーザーのstateなど
このような値は以下のようにContextAPIを使って管理しよう

LoadingProvider.tsx
import React from "react";

type LoadingContext = {
  setLoading: React.Dispatch<React.SetStateAction<boolean>>;
};

export const loadingContext = React.createContext<LoadingContext>({
  setLoading: () => null,
});

export function LoadingProvider(props: React.PropsWithChildren<{}>) {
  const [loading, setLoading] = React.useState(false);
  return (
    <loadingContext.Provider
      value={{
        setLoading,
      }}
    >
      {props.children}
    </loadingContext.Provider>
  );
}

コンポーネント毎のカスタムフックの役割のまとめ

dataflow (1).png

  • Providers
    • 各Pagesで使用したい変数/関数はここで生成する
  • Pages
    • このページ内の各コンポーネントで使用したい変数/関数はここで生成する
      • 商品一覧ページなら商品の配列などが該当する
      • ItemTable.tsxなら配列そのものを使用するし、ItemPagination.tsxなら配列の長さによってページ番号を動的に決定することになる
  • Organisms
    • そのコンポーネントで必要な変数や関数を生成する
    • 商品一覧テーブルならクリックした商品の詳細ページへ遷移させるとか
  • Molecules
    • Organismsと同様
  • Atoms
    • Organismsと同様

Material-UI関連

非デザイナーの自分はCSSのアセットとしてMaterial-UI(MUI)をよく使用しているので、それによって得た知見をまとめる

デザイン

ThemeProviderを活用しよう

defaultProps

MUIのボタンは以下の3種類の見た目が予め用意されている
スクリーンショット 2022-05-19 101854.png
これらを切り替えたい場合はvariantpropsを変えてやればOK

<Button variant="text">Text</Button>
<Button variant="contained">Contained</Button>
<Button variant="outlined">Outlined</Button>

ただし、このアプリではcontainedで統一したい、このアプリは基本outlinedを使いたいといった場合に、毎回variant="contained"と書くのは面倒だしチーム開発の場合は違うvariantを指定してしまう可能性もある

この問題を解決するためにはMUIのButtonを継承しvariantのデフォルト値を設定したAtomsコンポーネントを実装する手段もあるが、より手っ取り早い方法としてThemeProviderdefaultPropsを活用するのがおすすめ

ThemeProvider.tsx
import React from "react";
import { createTheme, ThemeProvider as MuiThemeProvider } from "@mui/material";

const theme = createTheme({
  components: {
    MuiButton: {
      defaultProps: {
        variant: "contained",
      },
    },
  },
});

export function ThemeProvider(props: React.PropsWithChildren<{}>) {
  return <MuiThemeProvider theme={theme}>{props.children}</MuiThemeProvider>;
}

このようなプロバイダーを実装し、アプリのルートコンポーネント(create-react-appで作った場合はindex.tsx)で呼び出すことでpropsのデフォルト値を変更できる

index.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ThemeProvider } from "./providers/ThemeProvider";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <React.StrictMode>
    <ThemeProvider>
      <App />
    </ThemeProvider>
  </React.StrictMode>
);

palette

ダークモード対応

独自定義のvariant

TODO

スタイリング

各コンポーネントのsizecolorなどのpropsでは対応できないくらいのスタイリングをしたい場合は以下の2つのAPIを使用する

styled関数

styled-componentsをMUI向けにラップした関数

import * as React from 'react';
import Slider, { SliderProps } from '@mui/material/Slider';
import { alpha, styled } from '@mui/material/styles';

const SuccessSlider = styled(Slider)<SliderProps>(({ theme }) => ({
  width: 300,
  color: theme.palette.success.main,
  '& .MuiSlider-thumb': {
    '&:hover, &.Mui-focusVisible': {
      boxShadow: `0px 0px 0px 8px ${alpha(theme.palette.success.main, 0.16)}`,
    },
    '&.Mui-active': {
      boxShadow: `0px 0px 0px 14px ${alpha(theme.palette.success.main, 0.16)}`,
    },
  },
}));

export default function StyledCustomization() {
  return <SuccessSlider defaultValue={30} />;
}

基本的な使い方はstyled-componentsと同様に、カスタマイズしたいコンポーネントにCSSを書き込んでラップする形になる
@mui/material/stylesstyled関数の特徴として、MUIのThemeオブジェクトを参照できるのでThemeProviderでカスタマイズした値を利用することができ、エディタのコード補完の恩恵を受けられる

CSSをラップしたコンポーネント内に閉じて再利用することが可能なので、いろいろな場面で使いまわしたい場合は↓のsxよりこちらのstyled関数の方が良い

sx props

各コンポーネントのsxpropsにCSSを渡す方法

<Slider
  defaultValue={30}
  sx={{
    width: 300,
    color: 'success.main',
  }}
/>

styled関数よりも手軽に書けるもののパフォーマンス的には劣る
なのでわざわざラップして再利用しやすくするまでもないCSSを書きたい場合に使用するのが良い

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?