Next.js中使用router.push()后如何显示加载Spinner?
解决React-Hook-Form + Shadcn表单提交后显示加载状态的问题
你的核心问题是:表单提交后触发路由跳转,页面查询参数变化会触发耗时的数据加载,但之前依赖表单自身提交状态的方案无法覆盖后续的数据加载过程,导致加载spinner无法持续显示。
为什么之前的方案失效?
form.formState.isSubmitting:仅在handleSubmit包裹的onSubmit函数执行期间为true。而你的onSubmit里只有同步的router.push,函数会快速执行完毕,isSubmitting立刻变回false,无法覆盖后续页面数据加载的耗时过程。useFormStatus().pending:同样只跟踪表单提交的pending状态,不包含路由跳转后的数据加载阶段。- 自定义
useState维护loading:路由跳转后组件会因查询参数变化重新渲染,新组件实例的loading状态会被重置为初始值,无法持续显示。
解决方案:绑定页面数据加载状态到表单按钮
需要将页面数据加载的状态与表单按钮的禁用/显示spinner逻辑关联。这里提供两种实现方式:
方式1:父组件传递加载状态(简单直接)
如果表单组件在页面组件内使用,可以在页面组件中维护数据加载状态,传递给表单:
page.tsx(客户端组件)
'use client'; import { useState, useEffect } from 'react'; import { useSearchParams } from 'next/navigation'; import DatePickerForm from './DatePickerForm'; export default function Page() { const searchParams = useSearchParams(); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchData = async () => { setIsLoading(true); // 替换为你的实际数据请求逻辑 await new Promise(resolve => setTimeout(resolve, 1500)); setIsLoading(false); }; fetchData(); }, [searchParams]); return ( <div> <DatePickerForm isLoading={isLoading} /> {/* 页面其他内容 */} </div> ); }
DatePickerForm组件
export function DatePickerForm({ isLoading }: { isLoading: boolean }) { const router = useRouter(); const { from } = useDateRange(); const form = useForm<z.infer<typeof FormSchema>>(); async function onSubmit(data: z.infer<typeof FormSchema>) { const from = format(data.from, "yyyy-MM-dd"); router.push(`/?from=${from}`); } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> {/* 原有表单字段部分保持不变 */} <div className="flex flex-row space-x-4"> <FormField control={form.control} name="from" render={({ field }) => ( <FormItem className="flex flex-col"> <FormLabel>Von</FormLabel> <Popover> <PopoverTrigger asChild> <FormControl> <Button variant="outline" className={cn( "w-[240px] pl-3 text-left font-normal", !field.value && "text-muted-foreground" )} > {field.value ? ( format(field.value, "PPP", { locale: de }) ) : ( <span>Pick date</span> )} <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> </Button> </FormControl> </PopoverTrigger> <PopoverContent className="w-auto p-0" align="start"> <Calendar mode="single" selected={field.value} onSelect={field.onChange} disabled={(date) => date > new Date() || date < new Date("1900-01-01") } initialFocus /> </PopoverContent> </Popover> <FormMessage /> </FormItem> )} /> </div> {/* 修改按钮部分 */} <Button disabled={isLoading} type="submit"> {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} Suchen </Button> </form> </Form> ); }
方式2:使用React Context共享状态(灵活解耦)
如果表单在多个地方使用,或者不想依赖父组件传参,可以用Context共享加载状态:
1. 创建LoadingContext
// app/loading-context.tsx 'use client'; import { createContext, useContext, useState, ReactNode } from 'react'; const LoadingContext = createContext<{ isLoading: boolean; setIsLoading: (value: boolean) => void; }>({ isLoading: false, setIsLoading: () => {}, }); export function LoadingProvider({ children }: { children: ReactNode }) { const [isLoading, setIsLoading] = useState(false); return ( <LoadingContext.Provider value={{ isLoading, setIsLoading }}> {children} </LoadingContext.Provider> ); } export function useLoading() { return useContext(LoadingContext); }
2. 在RootLayout中包裹Provider
// app/layout.tsx import { LoadingProvider } from './loading-context'; export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="de"> <body> <LoadingProvider>{children}</LoadingProvider> </body> </html> ); }
3. 在页面组件中更新加载状态
// app/page.tsx 'use client'; import { useEffect } from 'react'; import { useSearchParams } from 'next/navigation'; import { useLoading } from './loading-context'; import DatePickerForm from './DatePickerForm'; export default function Page() { const searchParams = useSearchParams(); const { setIsLoading } = useLoading(); useEffect(() => { const fetchData = async () => { setIsLoading(true); // 替换为你的实际数据请求逻辑 await new Promise(resolve => setTimeout(resolve, 1500)); setIsLoading(false); }; fetchData(); }, [searchParams, setIsLoading]); return ( <div> <DatePickerForm /> </div> ); }
4. 在表单组件中使用Context状态
export function DatePickerForm() { const router = useRouter(); const { from } = useDateRange(); const { isLoading } = useLoading(); // 从Context获取状态 const form = useForm<z.infer<typeof FormSchema>>(); async function onSubmit(data: z.infer<typeof FormSchema>) { const from = format(data.from, "yyyy-MM-dd"); router.push(`/?from=${from}`); } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> {/* 原有表单字段部分保持不变 */} <div className="flex flex-row space-x-4"> <FormField control={form.control} name="from" render={({ field }) => ( <FormItem className="flex flex-col"> <FormLabel>Von</FormLabel> <Popover> <PopoverTrigger asChild> <FormControl> <Button variant="outline" className={cn( "w-[240px] pl-3 text-left font-normal", !field.value && "text-muted-foreground" )} > {field.value ? ( format(field.value, "PPP", { locale: de }) ) : ( <span>Pick date</span> )} <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> </Button> </FormControl> </PopoverTrigger> <PopoverContent className="w-auto p-0" align="start"> <Calendar mode="single" selected={field.value} onSelect={field.onChange} disabled={(date) => date > new Date() || date < new Date("1900-01-01") } initialFocus /> </PopoverContent> </Popover> <FormMessage /> </FormItem> )} /> </div> {/* 修改按钮部分 */} <Button disabled={isLoading} type="submit"> {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} Suchen </Button> </form> </Form> ); }
内容的提问来源于stack exchange,提问作者christianbauer1
相关产品推荐
相关产品推荐

