3
1

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.

君はRecoilを直接操作していないか

Posted at

はじめに

今回はRecoilを直接component側でimportせず、カスタムフックでラッピングするという書き方が大変便利だったため紹介したい。

そもそもRecoilとはなんぞやという人は以下の記事もあるので参考にしてほしい。

なぜ私は直接操作しないのか

さっそく、なぜ私がRecoilを直接操作しないのかについて書いていく。
そんなことしなくてもRecoil端から導入しやすく、簡単に状態管理を行うことができるのではないかという声もあるだろう。
それでも私がRecoilを直接操作しないのには以下の理由がある。

- 状態の複雑化への対応が難しいため

Recoilは基本的に簡潔で直感的なAPIを提供しているが、アプリケーションの規模が大きくなるにつれて、状態の複雑化に対する対応が難しくなってくる。
カスタムフックでRecoilの状態管理のロジックを一箇所にまとめることで、管理しやすさやメンテナンス性の向上へ繋げることができる。

- 再利用性が低いため

コンポーネント内で直接RecoilのAPIを使用する場合、単一コンポーネントだけで状態管理が行われるため、他のコンポーネントで同じ状態を利用することができないという問題がある。
同じ状態を更新するロジックが複数のコンポーネントで必要な場合、それをカスタムフックにまとめることで、コードの再利用性を向上させることができる。
これにより、コードの重複が減り、保守性の向上へと繋げることができる。

- 可読性の向上のため

カスタムフックを介して状態を管理することで、Recoilの状態管理に関するコードを抽象化(隠蔽)し、コンポーネントの可読性が向上させることができる。

では以下から実際のカスタムフックの実装について見ていく。

実装内容

今回は一例としてRecoilを使ってサイトテーマを管理し、トグルボタンで切り替えられるように実装していく。
テーマは明るい「light」と「dark」の二種類を用意していく。
Videotogif.gif

目次

  • 環境
  • ディレクトリ構造
  • 前準備
  • 1. 定数を定義
  • 2. Recoilを定義
  • 3. カスタムフックを定義
  • 4. Toggleボタンを配置
  • 5. スタイルの切り替え

環境

  • Next.js
  • TypeScript
  • MUI

※前提条件

ディレクトリ構造

今回は以下で進めていく。

src/
└── features/
    ├── home/
    │   ├── components/
    │   │   ├── Home.tsx
    │   │   └── MaterialUISwitch.ts
    │   ├── constants/
    │   │   └── theme.ts
    │   ├── hooks/
    │   │   └── useTheme/
    │   │       └── index.ts
    │   └── stores
    │       └── index.ts
    ├── pages
    │   ├── _app.tsx
    │   └── index.tsx
    └── styles
        └── globals.css

前準備

今回の記事では説明しない部分についてあらかじめコードを用意しておく。

RecoilRootの設定

これを設定しないとRecoilが使えないため注意。

pages/_app.tsx
import type { AppProps } from "next/app";
import { RecoilRoot } from "recoil";
import "../styles/globals.css";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <RecoilRoot>
      <Component {...pageProps} />
    </RecoilRoot>
  );
}

globals.cssの設定

今回は既存の設定は不要のため最小限に修正。

styles/globals.css
* {
  margin: 0;
}

Togleボタンを用意

今回はMUI公式サイトのものを用意。

components/MaterialUISwitch.ts
import { styled, Switch } from "@mui/material";

const MaterialUISwitch = styled(Switch)(({ theme }) => ({
  width: 62,
  height: 34,
  padding: 7,
  "& .MuiSwitch-switchBase": {
    margin: 1,
    padding: 0,
    transform: "translateX(6px)",
    "&.Mui-checked": {
      color: "#fff",
      transform: "translateX(22px)",
      "& .MuiSwitch-thumb:before": {
        backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
          "#fff"
        )}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
      },
      "& + .MuiSwitch-track": {
        opacity: 1,
        backgroundColor: theme.palette.mode === "dark" ? "#8796A5" : "#aab4be",
      },
    },
  },
  "& .MuiSwitch-thumb": {
    backgroundColor: theme.palette.mode === "dark" ? "#003892" : "#001e3c",
    width: 32,
    height: 32,
    "&:before": {
      content: "''",
      position: "absolute",
      width: "100%",
      height: "100%",
      left: 0,
      top: 0,
      backgroundRepeat: "no-repeat",
      backgroundPosition: "center",
      backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
        "#fff"
      )}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
    },
  },
  "& .MuiSwitch-track": {
    opacity: 1,
    backgroundColor: theme.palette.mode === "dark" ? "#8796A5" : "#aab4be",
    borderRadius: 20 / 2,
  },
}));

export default MaterialUISwitch;

1. 定数を定義

はじめにRecoilで使う定数を定義する。
今回はテーマごとに、後程CSS操作の際に使うbackgroundColorとcolorを決めておく。

constants/theme.ts
export const THEME = {
  light: {
    backgroundColor: "#FFF",
    color: "#1D1F24",
  },
  dark: {
    backgroundColor: "#1D1F24",
    color: "#FFF",
  },
};

この様に定数を決めておくと、 仕様変更があった際も容易に修正することができる。
なお定数名は必ず大文字で明記する。

2. Recoilを定義

続いてRecoilを定義していく。

stores/index.ts
import { atom } from "recoil";
import { THEME } from "@/features/home/constants/theme";

export type Theme = keyof typeof THEME;

const themeState = atom<Theme>({
  key: "themeState",
  default: "light",
});
export { themeState };

ここではタイプを設定して"light"か"dark"以外の文字列は受け付けない様設定している。

export type Theme = keyof typeof THEME;

keyof typeofで記載することで、1で設定したTHEMEのキーをtypeとして設定することができる。
これでテーマの追加があった際でも定数の変更のみで対応ができる。

3. カスタムフックを定義

続いて本題のカスタムフックによるRecoilのラッピングを行なっていく。

hooks/useTheme/index.ts
import { useMemo } from "react";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { themeState } from "@/features/home/stores";
import { THEME } from "@/features/home/constants/theme";

const useTheme = () => {
  const setTheme = useSetRecoilState(themeState);

  const activeTheme = useRecoilValue(themeState);

  const themeStyle = useMemo(() => {
    return THEME[activeTheme];
  }, [activeTheme]);

  return useMemo(() => {
    return {
      setTheme,
      activeTheme,
      themeStyle,
    };
  }, [setTheme, activeTheme, themeStyle]);
};

export default useTheme;

今回エクスポートするのは以下の3つだ。

setTheme: Recoilの値の更新する
activeTheme: Recoilの値を取得する
themeStyle: Recoilの値に紐づいたスタイルを取得する

なお、themeStyleは"THEME[activeTheme]"とすることで1で定義したTHEMEから、backgroundColorとcolorが含まれたオブジェクトを返してくれる。

 ## 4. Toggleボタンを配置
ここがページのメインコンポーネントである。
今回はToggleボタンの切り替わりが分かるようRcoilの値を表示する場所を設けている。

Home.tsx
import { FormControlLabel, FormGroup } from "@mui/material";
import useTheme from "@/features/home/hooks/useTheme";
import MaterialUISwitch from "@/features/home/components/MaterialUISwitch";

const Home = () => {
  const { activeTheme, setTheme } = useTheme();

  const handleChange = () => {
    if (activeTheme === "light") setTheme("dark");
    else setTheme("light");
  };

  return (
    <>
      <h1>{activeTheme}</h1>
      <FormGroup>
        <FormControlLabel
          onChange={handleChange}
          control={<MaterialUISwitch sx={{ m: 1 }} />}
          label="テーマ"
          checked={activeTheme === "dark"}
        />
      </FormGroup>
    </>
  );
};
export default Home;

今回は値の表示とトグルボタンの操作で値の更新を行うため"activeTheme"と"setTheme"を呼び出している。

const { activeTheme, setTheme } = useTheme();

5. スタイルの切り替え

スタイルの切り替えはpages側で行なっていく。4で作成したコンポーネントをここで同時にインポートしていく。

pages/index.tsx
import Home from "@/features/home/components/Home";
import useTheme from "@/features/home/hooks/useTheme";

export default function HomePage() {
  const { themeStyle } = useTheme();

  return (
    <div
      style={{
        height: "100vh",
        width: "100vw",
        backgroundColor: themeStyle.backgroundColor,
        color: themeStyle.color,
      }}
    >
      <Home />
    </div>
  );
}

backgroundColorとcolorは1で作成した定数から引っ張ってくるため、カスタムフックのthemeStyleを呼び出すことで切り替えることができる。

      style={{
        height: "100vh",
        width: "100vw",
        backgroundColor: themeStyle.backgroundColor,
        color: themeStyle.color,
      }}

まとめ

以上がRecoilのラッピング実装例である。
初めは記述量が多いため面倒に感じるかもしれないが、慣れれば大変に使いやすいものでプロジェクト規模が大きくなる程それは実感できると思う。
Recoilを直接操作している場合は是非試してほしい。

3
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?