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に教えてください。

1 Likes
1 Likes