reactjsreact-hook-formreact-querytrpc.io

react-query, react-hook-form and form validation


I'm building a form (based on react-hook-form) to filter a list of events by start and end date. The list is retrieved with react-query's useQuery() call. There's a zod-based validation in place (via @hookform/resolvers) to make sure the end date always comes after the start date.

I tried various approaches but none of them really worked the way I expected (my examples use tRPC, but this shouldn't change things):

1. Introduce a dedicated state for the query which gets set in the onSubmit() handler.

const [query, setQuery] = useState({ startDate: initialStartDate, endDate: initialEndDate });
const { handleSubmit } = useForm({
  mode: 'onChange',
  schema: myValidationSchema,
});
const events = trpc.bookings.events.useQuery(query, {
  keepPreviousData: true
});

return (
  <form onSubmit={handleSubmit((values) => { setQuery(values) })}>
    ...
  </form>
)

This almost works, but it's somewhat weird that the query is automatically run when the component mounts. Also, what if there is no initial default state which satisfies the query? Imagine I don't want to default to a startDate and endDate but leave this for the user to fill out before running the query? {} does not satisfy the query constraints because startDate and endDate are required.

2. Get rid of the submit button and watch the form for changes. Then use the current form fields as the query payload.

I couldn't get this to work with the form validation. Queries were run although the form was in an invalid state.

const { watch } = useForm({
  mode: 'onChange',
  schema: myValidationSchema,
});
const query = watch();
const events = trpc.bookings.events.useQuery(query, {
  keepPreviousData: true,
});

I tried adding enabled: formState.isValid to the useQuery() options, but isValid does not represent the state of the validation accurately (probably due to its reactive nature). When logging it to the console, I can see it flapping between true and false in short succession when changing fields. The truthy state triggers the query with invalid values, which is what I'm trying to prevent in the first place.

3. Set enabled: false and refetch() in the onSubmit() handler.

This has the same drawback as the first two approaches: The query needs an initial state (which I might not have). Also, changing options in the form automatically updates the list of results when they have been fetched beforehand (because they're served from react-query's cache).

4. useMutation instead of useQuery

This would probably work best because it's easy to programmatically trigger the request with validated data from the form in the onSubmit() handler, but I haven't even tried it, because it just feels so wrong to use a POST request when a GET request is clearly the way to go here.

I feel like I must be doing something terribly wrong here, because the task is so common (filter a list of things by valid values from a form) and react-query is giving me such a hard time.


Solution

  • Interesting question, here are my thoughts for the approaches:

    1. You definitely need enabled in some way if you don't have initialData and / or do not want to run the query immediately.

    2. The fact that trpc does require you to provide an object with valid { startDate, endDate } is likely something you need to change. Make them optional or allow passing no parameter at all, then reject on the server if you don't have variables yet. This is similar to what I'm outlining here in my blog.

    3. Don't do enabled:false + refetch or useMutation. It's not the right tool for this.

    4. Watching with enabled: formState.isValid is neat but it really requires isValid to be "stable" across re-renders. What you are reporting sounds like a potential issue in react-hook-form.