You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 16:05:10