Remix中react-hot-toast重复触发8次的无障碍通知问题求助
盲态开发者Remix Todo应用无障碍通知重复播报问题排查
我是一名盲态Web开发者,正在用Remix构建简易Todo应用。功能正常,但成功添加Todo时的无障碍通知会重复播报8次。我用了role="status"属性,预期仅在内容变更时触发播报,但在NVDA+Chrome环境下,相同消息重复播报8次,求排查思路。
相关代码
首页组件(index route)
import toast, { Toaster, resolveValue } from "react-hot-toast"; import { json, redirect } from "@remix-run/node"; import type { ActionArgs, ActionFunction, LoaderFunction, } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigation, } from "@remix-run/react"; import { db } from "~/utils/db"; import type { todo } from "~/types/todo"; export const loader: LoaderFunction = async () => { const data = await db.todo.findMany(); return json(data); }; export const action: ActionFunction = async ({ request }: ActionArgs) => { const data = await request.formData(); const name = data.get("name"); const complete = data.get("complete") === null ? false : true; const description = data.get("description"); if ( typeof description !== "string" || typeof name !== "string" || typeof complete !== "boolean" ) { throw new Error("Form was not submitted Correctly"); } await db.todo.create({ data: { name, description, complete } }); return json({ msg: "Your form was submitted successfully." }); }; export default function Index() { const data = useLoaderData<todo[]>(); const nav = useNavigation(); const { msg } = useActionData() || {}; toast(msg); return ( <div> <h1>todo app</h1> <br /> <ul> {data.map((d) => ( <li key={d.id}> name:{d.name} <br /> description:{d.description} <br /> Complete{" "} <input type="checkbox" disabled={true} checked={d.complete} /> </li> ))} </ul> <br /> <h2>add</h2> <Form method="post"> <label htmlFor="name">Name:</label>{" "} <input type="text" id="name" name="name" /> <br /> <label htmlFor="description">Description:</label>{" "} <textarea rows={5} id="description" name="description" /> <br /> <label htmlFor="complete">Complete</label> <input type="checkbox" id="complete" name="complete" /> <button type="submit" disabled={nav.state !== "idle"}> Add </button> </Form> </div> ); }
根路由组件(root route)
import { Toaster } from "react-hot-toast"; import { cssBundleHref } from "@remix-run/css-bundle"; import type { LinksFunction } from "@remix-run/node"; import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; export const links: LinksFunction = () => [ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), ]; export default function App() { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <Meta /> <Links /> </head> <body> <Outlet /> <div role="status"> <Toaster></Toaster> </div> <ScrollRestoration /> <Scripts /> <LiveReload /> </body> </html> ); }
排查思路
- 修正toast调用时机:当前在组件顶层直接调用
toast(msg),组件每次渲染都会触发。改用useEffect包裹,仅当msg存在且变化时触发:useEffect(() => { if (msg) { toast(msg); } }, [msg]); - 排查组件渲染次数:在Index组件中添加
console.log("Index组件渲染"),查看是否渲染了8次——这会直接导致toast被调用8次。Remix在action完成、loader重新加载、导航状态变化时都会触发组件渲染。 - 调整无障碍属性范围:react-hot-toast的
Toaster本身可能已内置无障碍支持,外层的role="status"容器可能会监听内部所有DOM变更(比如toast的创建/销毁动画),导致多次播报。可以移除外层的role="status",或者给单个toast配置无障碍属性:toast(msg, { ariaProps: { role: "status", "aria-live": "polite" } }); - 避免重复创建toast:给toast设置唯一ID,确保相同消息不会重复创建,而是替换现有toast:
toast(msg, { id: "todo-submit-success" }); - 用无障碍工具追踪播报源:打开Chrome开发者工具的「无障碍」面板,查看每次播报对应的DOM元素和变更事件,确认是
role="status"容器的多次变更,还是toast组件的重复渲染导致的。
内容的提问来源于stack exchange,提问作者Chris Westbrook
相关产品推荐
相关产品推荐

