環境
- React 19.2.0
- @chakra-ui/react 3.29.0
- vite 7.2.2
実現方法
- 遷移にはReactのhooksの一つ
useNavigate()を利用 - toastの利用は公式を参照 公式
- 遷移条件としては無料のREST APIであるJSONPlaceholderを利用
Loginページの入力フィールドでの入力値がAPI上に存在すれば/Homeに遷移してtoastで"成功"と表示
うまくいかない例
login関数のthenで記載の通り、toastのロジックを記載したshowMessage関数の直後にnavigate('/Home')をコードする方法です。
この方法は結果として遷移に成功するものの、toastは実行されません。
showMessage()関数内部のconsole.log結果が正しく表示されるため実行されています。
しかし、遷移がただちに実行されることでtoastを確認することはできません。
/*
Loginページの入力フィールドにidを入力してボタンをクリックする。
クリック時にuseAuth.tsxで定義しているlogin関数を実行
**/
import { Button,Input } from "@chakra-ui/react";
import { memo, useState, type ChangeEvent } from "react";
import { Toaster } from "../../components/ui/toaster";
import { useAuth } from "../../hooks/useAuth";
export const Login = memo(() => {
const { login } = useAuth();
const [inputId, setInputId] = useState<string>("");
const onChangeInput = (event: ChangeEvent<HTMLInputElement>) => {
setInputId(event.target.value);
};
return (
<Input
placeholder="ユーザーID"
type="text"
value={inputId}
onChange={onChangeInput}
/>
<Button onClick={() => login(inputId)}>
ログイン
</Button>
<Toaster />
);
});
import axios from "axios";
import type { User } from "../types/api/user";
import { useNavigate } from "react-router-dom";
import { useCallback, useState } from "react";
import { toaster } from "../components/ui/toaster";
export const useAuth = () => {
//navigateのHooksを使用
const navigate = useNavigate();
//toastの作成
const showMessage = useCallback((result: string) => {
toaster.create({
title: `Toast status is ${result}}`,
duration:2000,
});
console.log("showMessage");
}, []);
const login = useCallback((id: string) => {
//APIを利用して値を取得
axios
.get<User>(`https://jsonplaceholder.typicode.com/users/${id}`)
.then((res) => {
//toastを表示させたのちにnavigateで遷移する
showMessage("成功");
navigate("/Home");
})
.catch(() => {
showMessage("失敗");
})
.finally(() => {});
}, []);
return { login };
};
解決策
解決策としては2通り考えられる。(下記のstackoverflow記事を参考)
- 遷移元でtoastの表示を待った後に遷移を実行させる
- 遷移元ではなく遷移先でtoastの表示を実行する
今回本記事では2番の方法で解説いたします。
うまくいく例(解決策2のコード)
- 遷移元でstateを引数で渡す(toastでの表示文字列が固定ならば不要)
- 遷移先でtoastの表示にはと
Promise.resolve().then(callback)を利用する
// 変更なし
//変更点のみ記載
/*useAuthでは記載しない. 定義したshowMessage関数も削除 **/
//showMessage("成功");
//遷移先で参照できるようにstateに保存
navigate("/Home", { state: { message: "成功" } });
import { memo, useCallback, useEffect } from "react";
import { useLocation } from "react-router-dom";
import { toaster, Toaster } from "../../components/ui/toaster";
export const Home = memo(() => {
//Login.tsxで保存したstateの取り出し
const location = useLocation();
//showMessage関数をこちらで定義する.hooksとして切り出してもよい。
const showMessage = useCallback((result: string) => {
toaster.create({
title: `Toast status is ${result}}`,
duration: 2000,
});
console.log("showMessage");
}, []);
//ここが重要
useEffect(() => {
Promise.resolve().then(() => {
showMessage(location.state.message);
});
}, []);
return (
<>
<p>Homeページです</p>
<Toaster />
</>
);
});
なぜPromise.resolve().then(callback)が必要か
こちらを使用しない場合、下記のようなエラーがログ上で表示されます。
※Chome環境において上記を書かなくてもtoast自体は正しく機能します。
Home.tsx:10 flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task.
エラーの内容を見るとレンダリング実行時にライブラリの関数(flushSync)が意図しない形で呼び出されているように見えます。
最後の一文のアドバイスの通り、処理が終わった後にshowMessage関数を呼び出す工夫が必要そうです。
javascriptはコードを非同期的に処理していくため(関数の処理完了をまたずに次の処理に進んでいく)ため生じると考えられます。
これを実現するために、コードでも記載しているPromise.resolve.then(callback)を利用します。このcallbackはすべてマイクロタスクと呼ばれ、その他の処理完了後に呼び出されるものです。
この内容についてはPADAoneさんのタスクキューとマイクロタスクキューを参考にしております。
マイクロタスク自体は、それを呼び出し関数やプログラムが実行された後にコールスタックが空になった後にのみ実行される短い関数です。API や Promise の then() メソッドなどの引数に渡すコールバック関数がマイクロタスクとして扱われます。
Promise.resolve().then(callback1).catch(callback2).finally(callback3) のように、Promise のプロトタイプメソッドである、then(), catch(), finally() の引数に渡すコールバック関数はすべてマイクロタスクとしてマイクロタスクキューへと送られます。
まとめ
Chacra-ui v2からv3にかけて大きな変更があったため、v2では問題なく動くにもかかわらずv3では同じ書き方だと動かないということに多く遭遇します。ただ単純にv2での書き方をv3での書き方に置換するだけだとハマるポイントが多いため、エラーメッセージをヒントに解決することが重要になりそうです。