1
/
5

React hooksでToastを実装するために

ヒョコッと現れてしばらくしたら消えてしまうという通知UIのトーストは、さまざまなコンポーネントから呼ばれる可能性があるため、気をつけて設計する必要がある。

あるコンポーネントでトーストを描写して使うようなケースでは、そのコンポーネントが消えるとトーストも消えてしまって予期しない挙動になる可能性がある。例えばポップアップメニューから何かを選択してトーストが表示され、すぐにメニューを閉じたような場合だ。

トーストを表示している間に消えてしまうようなコンポーネントであっても使えるトーストを設計したからここで自慢しておく。


この前トーストを作る機会があったのだが、そのときの要件は以下だった。

  1. 関数1つで呼び出せるぐらい簡易に使えること
  2. どのコンポーネントでも使えること
  3. 将来的に複数トーストを許すようにした場合でも対応できること

それが以下のコードになる。

// useToast.tsx

import React, { useState, createContext, useContext } from "react";
import { createPortal } from "react-dom";

type ToastTypes = "normal" | "error";

const ToastContext = createContext(({}: { text: string; type?: ToastTypes }) => {});
ToastContext.displayName = "ToastContext";

// useToastを使って深い階層のコンポーネントでもトーストを使えるようにする
export const useToast = () => {
  return useContext(ToastContext);
};


// 大元のコンポーネントを囲うためのProvider。トーストの実態もここに入れておく
export const ToastProvider: React.FC = ({ children }) => {
  const [showable, setShowable] = useState(false);
  const [toastText, setToastText] = useState("");
  const [toastType, setToastType] = useState<ToastTypes>("normal");

  const showToast = ({text, type = "normal"}: {text: string; type?: ToastTypes}) => {
    setToastText(text);
    setToastType(type);
    setShowable(true);
  };

  return (
    <ToastContext.Provider value={showToast}>
      {children}
      {createPortal(
        <Toast visible={showable} toastType={toastType}>
          {toastText}
        </Toast>,
        document.body
      )}
    </ToastContext.Provider>
  );
};

const Toast = styled.div<{ visible: boolean, toastType: ToastTypes }>`
  display: ${(p) => p.visible ? "block" : "none"};
  background-color: ${(p) => p.toastType === "normal" ? "blue" : "red"};
`;

まず ToastContextを作り、useContextを使ってuseToastを作った。この関数はトーストを使いたい各コンポーネントで使用されるものだ。

そして次にToastProviderを作った。これはToastContext.Providerを拡張したもので、内部でトーストの実態を生成している。ToastProviderを使ってコンポーネントを囲むと、トーストを表示する関数showToastが渡った状態のToastContext.Providerがそのコンポーネントを囲むようになる。


これらの関数を各コンポーネントで使えば、自由にトーストを表示させることができる。

// Parent.tsx

const Parent = () => {
  return (
    <ToastProvider>
      <Child />
    </ToastProvider>
  );
};

// Child.tsx

const Child = () => {
  const showToast = useToast();

  return (
    <Button onClick={() => showToast({text: "ボタンが押されました"})} />
  )
};

多分モーダルもおんなじ要領でできるはず。もっといい実装を思いついた人は記事を @intomyamに教えてください。

55 いいね!
55 いいね!