React hooksでToastを実装するために
ヒョコッと現れてしばらくしたら消えてしまうという通知UIのトーストは、さまざまなコンポーネントから呼ばれる可能性があるため、気をつけて設計する必要がある。
あるコンポーネントでトーストを描写して使うようなケースでは、そのコンポーネントが消えるとトーストも消えてしまって予期しない挙動になる可能性がある。例えばポップアップメニューから何かを選択してトーストが表示され、すぐにメニューを閉じたような場合だ。
トーストを表示している間に消えてしまうようなコンポーネントであっても使えるトーストを設計したからここで自慢しておく。
この前トーストを作る機会があったのだが、そのときの要件は以下だった。
- 関数1つで呼び出せるぐらい簡易に使えること
- どのコンポーネントでも使えること
- 将来的に複数トーストを許すようにした場合でも対応できること
それが以下のコードになる。
// 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に教えてください。