Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prefetchQuery in useHook() for client components #3

Closed
mmikhan opened this issue Feb 7, 2025 · 13 comments
Closed

prefetchQuery in useHook() for client components #3

mmikhan opened this issue Feb 7, 2025 · 13 comments

Comments

@mmikhan
Copy link

mmikhan commented Feb 7, 2025

Can we please get prefetchQuery in the useHook() hook for the client components? TanStack doc shows an example here. This is a must need to fill up the default values when creating a form using React Hook Form useForm() hook defaultValues: {}. Otherwise, we need the typical useEffect() to set the values.

@daveycodez
Copy link
Contributor

Yea I can add this today

@daveycodez
Copy link
Contributor

Can you show me an example of how you want to use this? I have some forms with initialValue and I simply don't render them if there isn't data available or isPending is true. You are trying to prefetch the session? What do you mean by add prefetchQuery in the useHook()? I want to make sure I cover this adequately for this use case

@daveycodez
Copy link
Contributor

daveycodez commented Feb 7, 2025

I was thinking of a new hook like const { prefetch } = usePrefetchSession() ?

@mmikhan
Copy link
Author

mmikhan commented Feb 7, 2025

I'll try my best to explain. Let's say you have the following component where you are using the user?.email in the defaultValues to pre-fill the data in the form. But it will not be populated when you visit the relevant component for the first time as useSession() take sometime due to stale time (if I'm not mistaken) thus, defaultValues fallback to empty string.

When you visit the relevant component next time, you'll see the form fields are being populated.

export function UpdateUserEmailSettingsForm() {
  const { user, isPending, refetch } = useSession();

  const form = useForm<UpdateUserEmailSettingsFormValues>({
    resolver: zodResolver(updateUserEmailSettingsSchema),
    defaultValues: {
      currentEmail: user?.email ?? "",
      newEmail: "",
    },
  });

  async function onSubmit(data: UpdateUserEmailSettingsFormValues) {
    console.log(data)
  }

  if (isPending || !user)
    return (
      <div className="flex h-[50vh] items-center justify-center">
        <Loader2 className="h-6 w-6 animate-spin" />
      </div>
    );

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
        <h3 className="mb-4 text-lg font-medium">Update Email Address</h3>
        <div className="space-y-4 rounded-lg border p-4">
          <FormField
            control={form.control}
            name="currentEmail"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Current Email</FormLabel>
                <FormControl>
                  <Input placeholder="Current Email" {...field} />
                </FormControl>
                <FormDescription>Your current email address.</FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="newEmail"
            render={({ field }) => (
              <FormItem>
                <FormLabel>New Email</FormLabel>
                <FormControl>
                  <Input placeholder="Your New Email" {...field} />
                </FormControl>
                <FormDescription>
                  Enter the new email address you wish to use.
                </FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />

          <Button
            type="submit"
            size={"sm"}
            variant={"outline"}
            disabled={
              isPending ||
              form.formState.isSubmitting ||
              !form.formState.isValid ||
              !form.formState.isDirty
            }
          >
            {isPending || form.formState.isSubmitting ? (
              <Loader2 className="animate-spin" />
            ) : (
              "Update Email"
            )}
          </Button>
        </div>
      </form>
    </Form>
  );
}

How can we solve this? There are a few ways:

  1. Prefetching the user

Prefetching the user is only available from server side at this point from your package. But client side prefetching isn't an option yet. If I server side prefetching here and use client side prefetching in other components, they won't be sync when the data is mutated from the client side. So, I'd need to router.refresh() to make sure both client and server side gets the new user data.

  1. Use useEffect
const form = useForm<UpdateUserEmailSettingsFormValues>({
  resolver: zodResolver(updateUserEmailSettingsSchema),
  defaultValues: {
    currentEmail: "",
    newEmail: "",
  },
});

useEffect(() => {
  if (user) {
    form.reset({
      currentEmail: user.email ?? "",
      newEmail: "",
    });
  }
}, [user, form]);

Using the typical useEffect(), we can set the user.email when the user data is available to us to populate the form fields. This could be avoided altogether if we could prefetch the user to makes sure defaultValues has access to user.email

  1. refetch option in Server side prefetch

Like the client side refetch option, offer a refetch option for server side as well.

const { user, refetch } = await prefetchSession(auth, queryClient, await headers());
  1. prefetch client side hook

Your proposal const { prefetch } = usePrefetchSession() will also work as long as we could prefetch the user during the mount. Would be nice to also have the refetch for server side as mentioned in #3

@daveycodez
Copy link
Contributor

daveycodez commented Feb 7, 2025

What I'm noticing from the Tanstack example linked is that the prefetching is for link hovers, etc. It doesn't actually prefetch on mount. I also don't think you want to prefetch on mount, since that would delay your rendering. I think adding prefetch on client side is still useful, but I don't think that is what you want for this form. Let's say you refresh the page on your form, the prefetch would either block the UI, and I don't think it's possible to prefetch client side (await in client component not supported)

What I would do is instead move your form to a separate component called <EmailForm /> then only render that when the user is available, so show your Skeleton with isPending. You would put your useForm inside of EmailForm, so that hook only runs after the user is loaded

@mmikhan
Copy link
Author

mmikhan commented Feb 7, 2025

Aha, thanks for digging into this. Prefetching on the client side will still be helpful though in your package. As well as an option to refetch the server side prefetch that you are offering already.

For now, I will use the suggestions you've shared or will stick to useEffect() method!

@daveycodez
Copy link
Contributor

daveycodez commented Feb 7, 2025

The "refetch" on server side prefetches should happen any time that that page SSR's, and since it uses headers it should be dynamic and refetching every render as is? The server side prefetch simply hydrates the new session into the useSession hook, and you can refetch it just fine there?

@daveycodez
Copy link
Contributor

daveycodez commented Feb 7, 2025

Also if you want the user to be available on initial render, that's when you would do the SSR prefetch, the useSession hook will be populated on render, which would be behind a Suspense boundary

@mmikhan
Copy link
Author

mmikhan commented Feb 7, 2025

Didn't work for me though when I used refetch() from the client component inside the onSuccess() from the example I shared above. I had to use router.refresh() to get the prefetch sharing the new data

@daveycodez
Copy link
Contributor

This is for rendering an email change? Or an authentication? This might be an issue with Next.js App Router cache

@daveycodez
Copy link
Contributor

daveycodez commented Feb 7, 2025

I'm gonna spend today working on forms and testing everything with updating email & avatar image, and making sure it works well with either SSR, client side or hybrid, including clearing cookie cache when one of these is updated. I'll share my findings when I'm done. It might be needed to call router.refresh() even still (for SSR) or revalidatePath, this could be a Next.js issue where it caches the page in the App Router client side

@mmikhan
Copy link
Author

mmikhan commented Feb 7, 2025

Great, I was testing it on the name and image for the user profile. Looking forward to the finding and the change you make on this 🙏

@daveycodez
Copy link
Contributor

I've included examples on how to use updateUser mutate function. I'm gonna close this issue for now. Feel free to open another issue if anything else comes up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants