⚒️

【hook設計】recoilでスナックバーを設計する

2022/11/15に公開

はじめに

今回スナックバーのリファクタリングをしていて,コンポーネントの設計について少し考える機会があったので共有させていただきます.
スナックバーなどの通知系のコンポーネントはトップあたりにひとつ置いておくだけで勝手に表示されるようにしたいです.

環境

React, Next.js, TypeScript, MUIなど

recoilとは?

Metaが作成しているReact向けの状態管理フレームワークです.

Reduxとは違い、全ての状態が 1 つの Store としてまとまっておらず、atom の組み合わせで状態を表現

書き味がReactのHookに似ているので直感的に理解することができます.
詳細についてはたくさん良い記事があるので参考に載せておきます.
現在(2022/11/13)では0.7.6が最新となっており,未だ1系のリリースがされていません.

MUI

今回はUIフレームワークとしてMUIを使用しています.
他のUIフレームワークや自作のデザインシステムを使用している場合はそれぞれの状況に合わせて置き換えながら読んでいただけたらと思います.

recoilをどう扱うか?

keyについて

recoilはatomを作成する際にプロジェクトで一意なkeyを設定する必要があります.
こちら(LINE証券の技術記事)ではRecoilKeys.tsというファイル内でenumを使ってkeyを管理しています.

しかしサバイバルTypeScriptや他の記事でも言及されているように型安全の面から少し扱いづらい部分があります.

ということで今回はオブジェクトリテラルを使ってkeyを管理することにしました.
こうすることでkeyの一意性を担保することができます.
具体的にはこんなファイルを作成しました.

recoilKeys.ts
export const RecoilAtomKeys = {
  SNACKBAR_STATE: 'snackbarState',
} as const

プロジェクトの方針にもよりますが,こちらのファイルはより上位のディレクトリに置いています.
今回はsrc/stores/に作成しました.

hooksについて

recoilのatom定義や機能の提供は全てhooks.tsというファイルに記述することにしました.

そしてこのhooks.tssrc/domains/snackbar/に置いています.
これもプロジェクトによって異なると思いますが,本プロジェクトではsrc/domainsの中にドメインに関わる機能(UIも含む)を作成するようにしています.

こうすることで新しい機能が必要な場合はsrc/domains/に作成すればよく,さらに使用しなくなった場合はそのディレクトリを削除するだけで済みます.

hooks.ts
import { AlertColor } from '@mui/material'
import { atom, useRecoilState, useSetRecoilState } from 'recoil'
import { RecoilAtomKeys } from '~/stores/recoilKeys'

type SNACKBAR_STATE = {
  isOpen: boolean
  text: string
  severity: AlertColor
}

type SNACKBAR_PARAMS = Pick<SNACKBAR_STATE, 'text' | 'severity'>

const snackbarStateAtom = atom<SNACKBAR_STATE>({
  key: RecoilAtomKeys.SNACKBAR_STATE,
  default: {
    isOpen: false,
    text: '',
    severity: 'info',
  },
})

export const useSnackbar = () => {
  const setSnackbarState = useSetRecoilState(snackbarStateAtom)

  const openSnackbar = ({ text, severity }: SNACKBAR_PARAMS) => {
    setSnackbarState({
      isOpen: true,
      text,
      severity,
    })
  }

  const closeSnackbar = () => {
    setSnackbarState({
      isOpen: false,
      text: '',
      severity: 'info',
    })
  }

  return { openSnackbar, closeSnackbar }
}

export const useCustomSnackbar = () => {
  const [snackbarState, setSnackbarState] = useRecoilState(snackbarStateAtom)

  return { snackbarState, setSnackbarState }
}

recoilはuseRecoilValueuseSetRecoilStateuseRecoilStateというようなAPIがありますが,それらをむき出しにしたくないのでラップしたものをexportします.

例外的にuseCustomSnackbarはほぼ何もせずに機能を提供していますが,こちらはUIコンポーネントで使用するためだけのカスタムフックです.

実際に使用する際は

const { openSnackbar, closeSnackbar } = useSnackbar()
...

みたいに使う形になります.

ここで考慮した点は,

  • ユーザーが使いたいのはスナックバーを開く,閉じるという行為のみ
  • 開くときはメッセージ(text)とメッセージの重大度(severity)だけを指定したい

です.これを考慮することで使う側も非常に直感的に使用することができます.

UIについて

先ほどのhooks.tsで定義したuseCustomSnackbarをここで使います.

CustomSnackbar.tsx
import { Alert, Snackbar } from '@mui/material'
import { useCustomSnackbar, useSnackbar } from './hooks'

export const CustomSnackbar = () => {
  const { snackbarState } = useCustomSnackbar()
  const { closeSnackbar } = useSnackbar()

  return (
    <Snackbar
      open={snackbarState.isOpen}
      autoHideDuration={6000}
      onClose={closeSnackbar}
    >
      <Alert
        onClose={closeSnackbar}
        severity={snackbarState.severity}
        sx={{ width: '100%' }}
      >
        {snackbarState.text}
      </Alert>
    </Snackbar>
  )
}

ここはプロジェクトで使用しているUIフレームワークに合わせて作成していただければと思います.

useCustomSnackbarをexportしたくないと考えるならばhooks.tsCustomSnackbarをレンダリングする関数を返すようにしても良いかもしれません.(render hooksパターンと呼ばれるやつです)

今回はUIとhookを完全に分けたかったので上のような設計にしました.
こうすることで実はロジック部分をrecoilではなくuseContextを使った場合でもUIコンポーネントは変更せずにすみます.
(このファイルはMUIとhooks.tsにしか依存していません)

使い方

コンポーネントで使う場合はとてもシンプルに使うことができます.

sample.tsx
import { useSnackbar } from '~/domains/snackbar'

const Sample = () => {
  const { openSnackbar } = useSnackbar()
  
  const onClick = () => {
    openSnackbar({
      text: 'クリックされました',
      severity: 'info'
    })
  }
  ...
}

また_app.tsxなどに
RecoilRootCustomSnackbarを配置すればどのコンポーネントからでもスナックバーを使うことができます

おわりに

この設計はプロジェクトのディレクトリ構成に少し依存する部分があるので全ての方におすすめできるものではありませんが,今の状況では使いやすい設計だと思うので共有させていただきました.

もっと良い設計や,ここもう少しこうした方が良いという部分などありましたぜひコメントお願いします!

参考

https://recoiljs.org/
https://uit-inside.linecorp.com/episode/49
https://engineering.linecorp.com/ja/blog/line-securities-frontend-3/
https://engineering.linecorp.com/ja/blog/line-sec-frontend-using-recoil-to-get-a-safe-and-comfortable-state-management/

Discussion