【React】一時的な通知メッセージ(Toast)を表示する
更新日:2026/02/13
ブラウザ上でふわっと現れて少し経つとふわっと消えていく通知メッセージを、よく見るようになりました。
このようなメッセージをToastと呼びます。
画面の端や中央に突然出てくる様子がトースターから飛び出すパンに似ているから、というのが由来だそうです。
今回は、ReactでToastを実装してみようと思う。
概要(仕様)
ざっくりと、次のような仕様で作成しようと思う。
- ブラウザの中心に表示する
- コンポーネントから関数呼び出しで表示
- キューを使用して順番に表示
- 複数設置は考慮しない
今回は二つのアプローチでToastを実現します。
一つはReactの機能のみでToastを表示します。
この方法は使用上の制限があります。
Toast目的には少し使いにくいですが、今回は学習目的で作成しました。
→Context版Toast
もう一つはReactの外部に仕組みを作ることで、関数呼び出しによるToast表示を行います。
コードをインポートすればどこからでも呼び出すことができるのが魅力です。
→関数呼び出し版Toast
Context版Toast
Contextを使用すると、コンポーネントの親子ツリー内で関数を含めた共通の値を利用できます。
そこでToastを管理するコンポーネントをContext化して、Toast表示関数をツリー内のコンポーネントに提供します。
https://note.affi-sapo-sv.com/demo/react-toast/
■GitHub
https://github.com/kchan-p/react-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等で上書き可能にしたい場合は、設計を変える必要がありますね。
エラーになっちゃった!
■【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;
/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;
これで完成。
ビルドしてデモ化しました。
https://note.affi-sapo-sv.com/demo/react-toast/
■GitHub
https://github.com/kchan-p/react-toast
関数呼び出し版Toast
関数呼び出し版Toastは、Context版Toastのtoastprovider.jsxをベースにします。
具体的にはコンポーネント内で定義しているshowToast関数をリスナーとして外部に渡します。
外部の関数が渡されたリスナーを呼び出すことで、Toastコンポーネントのステートを制御して、Toastを表示します。
https://note.affi-sapo-sv.com/demo/react-toast-function/
■GitHub
https://github.com/kchan-p/react-toast-function
Toastの完成コード
関数呼び出し版のファイルは、3つです。
toast ┣━ toast.js ┣━ toast.jsx ┗━ toast.modules.css
- toast.js:
Toast表示関数とリスナー登録関数を提供するファイル。 - toast.jsx:
toastprovider.jsxをベースとして作成した、Toastコンポーネント。 - toast.modules.css:
Context版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版Toastのcountercontext.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;
これで完成。
こちらもビルドしてデモ化しました。
https://note.affi-sapo-sv.com/demo/react-toast-function/
■GitHub
https://github.com/kchan-p/react-toast-function
更新日:2026/02/13
関連記事
スポンサーリンク
記事の内容について

こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。

