React

【React】一時的な通知メッセージ(Toast)を表示する

更新日:2026/02/13

ブラウザ上でふわっと現れて少し経つとふわっと消えていく通知メッセージを、よく見るようになりました。
このようなメッセージをToastと呼びます。
画面の端や中央に突然出てくる様子がトースターから飛び出すパンに似ているから、というのが由来だそうです。

今回は、ReactでToastを実装してみようと思う。

 

概要(仕様)

ざっくりと、次のような仕様で作成しようと思う。

  • ブラウザの中心に表示する
  • コンポーネントから関数呼び出しで表示
  • キューを使用して順番に表示
  • 複数設置は考慮しない

今回は二つのアプローチでToastを実現します。

一つはReactの機能のみでToastを表示します。
この方法は使用上の制限があります。
Toast目的には少し使いにくいですが、今回は学習目的で作成しました。
Context版Toast

もう一つはReactの外部に仕組みを作ることで、関数呼び出しによるToast表示を行います。
コードをインポートすればどこからでも呼び出すことができるのが魅力です。
関数呼び出し版Toast

 

Context版Toast

Contextを使用すると、コンポーネントの親子ツリー内で関数を含めた共通の値を利用できます。
そこでToastを管理するコンポーネントをContext化して、Toast表示関数をツリー内のコンポーネントに提供します。

Toastの完成コード

今回は、次の4つのファイルでToastを実現しています。

toast
 ┣━ toastcontext.js
 ┣━ toastprovider.jsx
 ┣━ usetoast.js
 ┗━ toast.modules.css
  • toastcontext.js:
    Contextを作成。toastprovider.jsxとusetoast.jsでインポートされる。
  • toastprovider.jsx:
    Toast表示のロジックを含んだContextプロバイダ。
  • usetoast.js:
    子コンポーネント側で使用するカスタムフック
  • toast.modules.css:
    Toastのスタイル定義

toastcontext.js

最初はtoastcontext.jsです。

createContext()を使ってContextを作成します。
Contextはコンポーネントで使いまわすのでエクスポートしておきます。

toastcontext.js

import { createContext } from "react";

const counterContext = createContext(null);
export default counterContext;

toastprovider.jsx

toastprovider.jsxは、Contextプロバイダです。
Toast表示のロジックを組み立てていきます。

toastprovider.jsx

import { useState, useRef } from "react";
import  ToastContext from "./toastcontext";
import styles from "./toast.module.css";

function ToastProvider({ children }){

    const {showToast,message,isFadeIn} = useToastContext();

    return (
        <ToastContext value={{ showToast }}>
            {children}
            {message && <div
                className={
                    `${styles.toast} ${isFadeIn ? styles.toast_fadein : styles.toast_fadeout}`
                }
            >
                {message.msg}
            </div>}
        </ToastContext>
    );
};
export default ToastProvider;

const useToastContext = ()=>{
    const [message, setMessage] = useState(null);
    const [isFadeIn, setIsFadeIn] = useState(false);

    const busyRef = useRef(false);
    const queueRef = useRef([]);

    const FADE_OUT = 300;

    const nextToast = () => {
        // キュー存在確認
        if (queueRef.current.length === 0) {
            busyRef.current = false;
            return;
        }
        busyRef.current = true;
        // キューからメッセージ取得
        const { msg, duration } = queueRef.current.shift();

        const waitTime = duration - FADE_OUT;
        // メッセージステータスセット→再レンダリング
        setMessage({msg});
        // フレームをずらす
        requestAnimationFrame(() => setIsFadeIn(true));

        setTimeout(() => {
            setIsFadeIn(false);

            setTimeout(() => {
                setMessage(null); 
                nextToast();
            }, FADE_OUT);
        }, waitTime < 0 ? duration : waitTime );
    };
    // コンポーネントが呼び出す関数
    const showToast = (msg, duration = 2000) => {
        queueRef.current.push({ msg, duration });
        if (!busyRef.current) {
            nextToast();
        }
    };
    return {showToast,message,isFadeIn};
}

ToastProviderプロバイダが受け取っている children は、ToastProviderプロバイダでラップした子コンポーネントです。
今回はApp.jsxで、ラップしています。

ToastProviderプロバイダはuseToastContext関数を呼び出していますが、初期コードでは関数化せずにそのまま記述されていました。
今回は学習目的ということでContextの構造をわかりやすくするために、関数化しています。

Contextで重要なのがvalue値です。
value値で指定した値は、子コンポーネントでuseContextフックを呼び出すことで取り出すことができます。

今回は次のように、showToast関数を渡しています。

    return (
        <ToastContext value={{ showToast }}>
            // ...省略
        </ToastContext>
    );

ちなみにvalue値にはuseStateフックで取得したセット関数を渡すことができます。
セット関数を子コンポーネントが呼び出すとContextプロバイダ内のコンポーネントが再レンダリングされます。
不用意にセット関数が呼び出されると不必要な再レンダリングが発生する可能性があるので、渡す値は十分な検討が必要です。

今回はメッセージをキューに入れる必要があるので、showToast関数で交通整理を行っています。

usetoast.js

usetoast.jsは、子コンポーネントが呼び出すことを目的としたカスタムフックです。

usetoast.js

import { useContext } from "react";
import ToastContext from "./toastcontext";

function useToast(){
  const context = useContext(ToastContext);
  if (!context) {
    throw new Error("useToast は ToastProvider 内で使用する必要があります");
  }
  return context;
};

export default useToast;

実際のところは、この関数は必要ありません。
子コンポーネント内で次のコードを呼び出すだけでも大丈夫です。

const context = useContext(ToastContext);

toast.modules.css

toast.modules.cssは、Toastのスタイルを定義したcssモジュールです。

toast.modules.css

.toast {
    position: fixed;
    top: 50%;
    left: 50%;
    padding: 5px 16px;
    background: #333;
    color: #fff;
    border-radius: 10px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
    transform: translate(-50%, -50%) translateY(10px);
    transition: opacity 0.3s ease, transform 0.3s ease;
    opacity: 0;
}
.toast_fadein {
    opacity: 1;
    transform: translate(-50%, -50%) translateY(0);
}
.toast_fadeout {
    opacity: 0;
    transform: translate(-50%, -50%) translateY(10px);
}

今回はスタイル固定です。
外部CSS等で上書き可能にしたい場合は、設計を変える必要がありますね。

toastprovider.jsxはわかりにくいから、ToastProvider.jsxの方がいいよねと思って変更したら...
エラーになっちゃった!
【VSCode】Windowsでts(1149)エラーが出た時の対処法
めんどくさいよね。というお話

テスト(デモ)用コード

Toast用のReactコードが完成したので、次はテストです。
Vite環境でテスト用のコードを作成しました。

react-toast
 ┣━ src
 ┃   ┣━ assets
 ┃   ┣━ test
 ┃   ┃   ┣━ countercontext.jsx
 ┃   ┃   ┣━ counterprovider.jsx
 ┃   ┃   ┗━ testbutton.jsx
 ┃   ┣━ toast
 ┃   ┃   ┣━ toast.modules.css
 ┃   ┃   ┣━ toastcontext.js
 ┃   ┃   ┣━ toastprovider.jsx
 ┃   ┃   ┗━ usetoast.js
 ┃   ┣━ App.jsx
 ┃   ┗━ main.jsx
 ┣━ index.html
 ┣━ package-lock.json
 ┣━ package.json
 ┗━ vite.config.js

App.jsxはToastProviderプロバイダを呼び出しています。

/src/App.jsx

import ToastProvider from "./toast/toastprovider";
import TestButton from "./test/testbutton";
import CounterProvider from "./test/counterprovider";

function App() {
  return (
    <>
      <h1>Reactでの一時的な通知メッセージ(Toast)</h1>
      <ToastProvider>
        <CounterProvider>
          <TestButton clickNum={1} />
          <TestButton clickNum={3} />
        </CounterProvider>
      </ToastProvider>
    </>
  );
}
export default App;

counterprovider.jsxのCounterProviderもContextプロバイダです。

数値をカウントアップするContextを作成しています。

/src/test/counterprovider.jsx

import { useRef } from "react";
import CounterContext from "./countercontext";

function CounterProvider({ children }) {
  const counterRef = useRef(0);

  const countIncrement = () => {
    counterRef.current += 1;
    return counterRef.current;
  };

  return (
    <CounterContext value={{ countIncrement }}>
      {children}
    </CounterContext>
  );
}

export default CounterProvider;

countercontext.jsxはcreateContext()の結果を返すだけです。

countercontext.js

import { createContext } from "react";

const counterContext = createContext(null);
export default counterContext;

testbutton.jsxは、ボタンが押されたらToastとカウンターの二つのContextを利用して通知を行うコンポーネントです。

/src/test/testbutton.jsx

import { useContext } from "react";
import useToast from "../toast/useToast";
import counterContext from "./countercontext";

function TestButton({ clickNum }) {
    const { showToast } = useToast();
    const { countIncrement } = useContext(counterContext);

    return (
        <>
            <button onClick={
                () => {
                    for (let i = 0; i < clickNum; i++) {
                        const count = countIncrement();
                        showToast(`通知(${count}回目)`);
                    }
                }
            }
            >通知!!{clickNum > 1 ? `(${clickNum}連続)` : ""}</button>
        </>)
}

export default TestButton;

これで完成。

ビルドしてデモ化しました。

 

関数呼び出し版Toast

関数呼び出し版Toastは、Context版Toasttoastprovider.jsxをベースにします。

具体的にはコンポーネント内で定義しているshowToast関数をリスナーとして外部に渡します。
外部の関数が渡されたリスナーを呼び出すことで、Toastコンポーネントのステートを制御して、Toastを表示します。

Toastの完成コード

関数呼び出し版のファイルは、3つです。

toast
 ┣━ toast.js
 ┣━ toast.jsx
 ┗━ toast.modules.css

toast.js

toast.jsは、次のようになっています。

toast.js

let toastListener = null;

    // Toastコンポーネントからのリスナー登録
export function addToastListener(listener) {
    toastListener = listener;
    return () => toastListener = null;
}
    // Toast表示関数
export function showToast(message, duration) {
    const dur = typeof duration !== "number" ? 3000 : duration;
    if( toastListener ) toastListener(message,dur);
}

非常に単純なコードですね。
addToastListenerは、useEffect()で呼び出されることを前提としています。
そのため戻り値は、後始末関数です。

このコードで問題なのは二つ以上のToastコンポーネントを設置した場合、toastListener が上書きされてしまう点です。
今回はこの点を考慮していません。

考慮する場合は配列化して、Toastコンポーネント側から名前指定できるようにするとよさそうです。
コンポーネント毎にスタイルを変更する仕組みも必要ですね。

toast.jsx

toast.jsxは、toastprovider.jsx内のshowToast関数をtoast.jsのaddToastListener関数に渡すだけ...と思ったら、罠が多かった。

toast.jsx

import { useEffect, useState, useRef, useCallback } from "react";
import {
    addToastListener,
} from "./toast.js";
import styles from "./toast.module.css";

const FADE_OUT = 300;

export default function Toast() {
    const [message, setMessage] = useState(null);
    const [isFadeIn, setIsFadeIn] = useState(false);

    const busyRef = useRef(false);
    const queueRef = useRef([]);

    // useCallback内での自己呼び出しがエラーになるため、
    // refで関節参照する
    const nextToastRef = useRef(null);
    const nextToast = useCallback(
        () => {
            // キュー存在確認
            if (queueRef.current.length === 0) {
                busyRef.current = false;
                return;
            }
            busyRef.current = true;
            // キューからメッセージ取得
            const { msg, duration } = queueRef.current.shift();

            const waitTime = duration - FADE_OUT;
            // メッセージステータスセット→再レンダリング
            setMessage({ msg });
            // フレームをずらす
            requestAnimationFrame(() => setIsFadeIn(true));
          
            setTimeout(() => {
                setIsFadeIn(false);

                setTimeout(() => {
                    setMessage(null);
                    nextToastRef.current();
                }, FADE_OUT);
            }, waitTime < 0 ? duration : waitTime);
        }, []);

    // React Compilerはrender中のref書き換え禁止、に対応
    useEffect(() => {
        nextToastRef.current = nextToast;
    }, [nextToast]);

    const showToast = useCallback(
        (msg, duration) => {
            queueRef.current.push({ msg, duration });
            if (!busyRef.current) {
                nextToast();
            }
        }, [nextToast]);

    useEffect(() => {
        return addToastListener(showToast);
    }, [showToast]);

    return (
        <div id="toast_wrap">
            {message && <div className={
                `${styles.toast} ${isFadeIn ? styles.toast_fadein : styles.toast_fadeout}`
            }
            >
                { message.msg}
            </div>}
        </div>
    );
}

まずはコンポーネント内の関数をuseCallbackフックでラップします。
するとnextToast関数内でのnextToast呼び出しでeslintがエラーを検出しました。

『Error: Cannot access variable before it is declared』

関数内で自分自身を参照するとこのエラーがでるようです。
そこで、useRefフックで間接的な参照状態に変更します。

ですが、useRefフックのcurrentに関数をセットすると、今度は次のエラーがでます。

『Error: Cannot access refs during render』

render中のref値書き換えが禁止されているのが原因のようです。
そこでuseEffectでref値を変更します。

テスト(デモ)用コード

次はテストですね。
Vite環境でテスト用のコードを作成しました。

ファイル構成は次のようになります。

react-toast-function
 ┣━ src
 ┃   ┣━ assets
 ┃   ┣━ test
 ┃   ┃   ┣━ countercontext.jsx
 ┃   ┃   ┣━ counterprovider.jsx
 ┃   ┃   ┗━ testbutton.jsx
 ┃   ┣━ toast
 ┃   ┃   ┣━ toast.js
 ┃   ┃   ┣━ toast.jsx
 ┃   ┃   ┗━ toast.module.css
 ┃   ┣━ App.jsx
 ┃   ┗━ main.jsx
 ┣━ index.html
 ┣━ package-lock.json
 ┣━ package.json
 ┗━ vite.config.js

App.jsxは次のようになっています。

App.jsx

import TestButton from "./test/testbutton";
import CounterProvider from "./test/counterprovider";
import Toast from "./toast/toast.jsx";

function App() {

  return (
    <>
      <h1>Reactでの一時的な通知メッセージ(Toast)関数呼び出し版</h1>
      <CounterProvider>
        <TestButton clickNum={1} />
        <TestButton clickNum={3} />
      </CounterProvider>
      <Toast />
    </>
  );
}
export default App;

CounterProviderは、countercontext.jsxとcounterprovider.jsxで構成される、数値のインクリメント用Contextです。
これらのコードはContext版Toastcountercontext.jsxおよびcounterprovider.jsxと同じものです。

TestButtonは、testbutton.jsxで定義しています。
こちらはContext版Toastの同名ファイルと少し異なります。
ボタンが押されたらshowToast関数を呼ぶのは同じですが、showToast関数の取得元が異なります。

/src/test/testbutton.jsx

import { useContext } from "react";
import counterContext from "./countercontext";
import { showToast } from "../toast/toast";

function TestButton({ clickNum }) {
    const { countIncrement } = useContext(counterContext);

    return (
        <>
            <button onClick={
                () => {
                    for (let i = 0; i < clickNum; i++) {
                        const count = countIncrement();
                        showToast(`通知(${count}回目)`);
                    }
                }
            }
            >通知!!{clickNum > 1 ? `(${clickNum}連続)` : ""}</button>
        </>)
}

export default TestButton;

これで完成。

こちらもビルドしてデモ化しました。

更新日:2026/02/13

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。