loading...
Cover image for How to move to react-query v1

How to move to react-query v1

thatzacdavis profile image Zachary Davis ・5 min read

react-query v1 is here! And with it comes several breaking changes that will render your code useless if you try to upgrade without making any changes. This guide will cover several examples of old code using react-query v0.4.3 or older, and how to get the same functionality out of v1.0.*.

Firstly, if you are unfamiliar with react-query, this guide won't be too useful for you. Instead, I recommend jumping right into the Readme if you want a simple, hook-based library to help you make API calls.

If you do have code using an older version, however, look no further!

Pagination

We're going to start with pagination, as this is the first breaking change in v1. Gone is the paginated flag on the useQuery hook. Instead, there is a new usePaginatedQuery hook.

  const {
    status,
    resolvedData,
    latestData,
    error,
    isFetching,
  } = usePaginatedQuery(['projects', page], fetchProjects);

As you can see, the code is fairly similar, and we still are able to find out if the query is still running via the isFetching status, but there is no longer an isFetchingMore function to call as we are only receiving one page's worth of data at a time here.

data does not exist when using this hook, however. Now, there are two different objects that contain data, resolvedData and latestData:

  1. resolvedData is data from the last known successful query result at first, and will remain the same until the next query successfully resolves.

  2. latestData is only ever the data from the latest query, so it will be undefined until the latest query resolves.

Both objects can be mapped over similarly to how one might use data, like this:

<div>
  {resolvedData.projects.map(project => (
    <p key={project.id}>{project.name}</p>
  ))}
</div>

Other than that, we are just syntactically telling the library to do a paginated query in a different way: specifying the hook instead of having a more generic one where we pass in whether things are paginated or not.

This makes the resulting code a little cleaner in my opinion than how we used to do it, which involved the same generic useQuery hook that was available in v0.4.3, and telling it that it was paginated like this:

const { data,
    isLoading,
    isFetching,
    isFetchingMore,
    fetchMore,
    canFetchMore,
  } = useQuery(
    'projects',
    ({ nextId } = {}) => fetch('/api/projects?page=' + (nextId || 0)),
    {
      paginated: true,
      getCanFetchMore: lastPage => lastPage.nextId,
    }
  );

Infinite Loading

Previously, if you wanted to load more, you had to implement a loadMore function like so:

const loadMore = async () => {
    try {
      const { nextId } = data[data.length - 1];

      await fetchMore({
        nextId,
      });
    } catch {}
  };

While this is still the case if you want to manually control which pages you load and when it is no longer necessary to continuously call a function like that if you want to implement an infinite load to get all of the resources from your API.

The useInfiniteQuery is aptly named to help you get that job done, all while providing an interface similar to that of the olduseQuery:

const {
    status,
    data,
    isFetching,
    isFetchingMore,
    fetchMore,
    canFetchMore,
  } = useInfiniteQuery('projects', fetchProjects, {
    getFetchMore: (lastGroup, allGroups) => lastGroup.nextCursor,
  });

useQuery used to be able to do this for you, separating this functionality out into its own hook helps make the code a little more clear and readable in my opinion.

useMutation

The useMutation hook has also changed quite a bit. Gone are the refetchQueries and updateQuery options. Now, we have onSuccess, onError, and onSettled callbacks. These align to how people had been using the library anyhow, and are a welcome change.

If you want to run a query every time you update a variable, you can do something like this:

const [mutatePostTodo] = useMutation(
    text =>
      fetch('/api/data', {
        method: 'POST',
        body: JSON.stringify({ text }),
      }),
    {
      // to revalidate the data and ensure the UI doesn't
      // remain in an incorrect state, ALWAYS trigger a
      // a refetch of the data, even on failure
      onSettled: () => queryCache.refetchQueries('todos'),
    }
  );

async function handleSubmit(event) {
    event.preventDefault();
    // mutate current data to optimistically update the UI
    // the fetch below could fail, so we need to revalidate
    // regardless

    queryCache.setQueryData('todos', [...data, text]);

    try {
      // send text to the API
      await mutatePostTodo(text);
      setText('');
    } catch (err) {
      console.error(err);
    }
  }

This not only updates the todo that you've edited but it then goes and gets the list again whether or not the POST call was successful. If you wanted to only update your list when the POST was successful, then you could swap out onSettled for onSuccess. Similarly, you could use onError for a failure condition. If you want to throw an exception as well when an error occurs, you can use the throwOnError function.

In the past, if you wanted to refetch your todos no matter, you had to do something like this:

const [mutatePostTodo] = useMutation(
    text =>
      fetch('/api/data', {
        method: 'POST',
        body: JSON.stringify({ text }),
      }),
    {
      refetchQueries: ['todos'],
      // to revalidate the data and ensure the UI doesn't
      // remain in an incorrect state, ALWAYS trigger a
      // a refetch of the data, even on failure
      refetchQueriesOnFailure: true,
    }
  );

  async function handleSubmit(event) {
    event.preventDefault();
    // mutate current data to optimistically update the UI
    // the fetch below could fail, so we need to revalidate
    // regardless

    setQueryData('todos', [...data, text], {
      shouldRefetch: false,
    })

    try {
      // send text to the API
      await mutatePostTodo(text);
      setText('');
    } catch (err) {
      console.error(err);
    }
  }

While the functionality is the same, the syntax in v1 is much more declarative, and the new methods allow for much more flexibility.

QueryCache

The previous example of how to refetch todos in v1 also illustrates how to use the new queryCache methods. The queryCache remembers all of your queries and the settings of those queries.

You can now easily refetchQueries by ID or refetchQueries by itself, which replaces refetchAllQueries. You can also prefetchQuery, setQueryData, clear, or removeQueries.

These also replace the standalone functions of refetchQuery, refetchAllQueries, prefetchQuery, updateQuery, and clearQueryCache.

useQuery

The main hook in the library, useQuery has also undergone some other minor changes. Gone is the isLoading boolean status, instead, there is a status string returned with different possible values. isFetching, however, has remained unchanged.

Previously we could track the status like this:

  const { data, isLoading, isFetching } = useQuery('projects', () =>
    fetch('/api/data')
  );

Now, we do this:

  const { status, data, error, isFetching } = useQuery('projects', () =>
    fetch('/api/data')
  );

Status can either be success, loading, or error when returned from useQuery and the previously mentioned useMutation, but success can generally be assumed when we are not in a loading or error state with some JSX like this:

    <div style={{ textAlign: 'center' }}>
      <h1>Trending Projects</h1>
      <div>
        {status === 'loading' ? (
          'Loading...'
        ) : status === 'error' ? (
          <span>Error: {error.message}</span>
        ) : (
          <>
            <div>
              {data.map(project => (
                <p key={project}>
                  <Link href="/[user]/[repo]" as={`/${project}`}>
                    <a>{project}</a>
                  </Link>
                </p>
              ))}
            </div>
          </>
        )}
      </div>
    </div>

Essentially, if we are not loading data, and don't have an error, we should have data to show the user.

Keys

Keys are another concept that has changed with the v1 release of react-query. String only keys are still supported (they are converted to arrays under the hood), but keys with associated variables are not limited just a tuple format anymore. Instead, keys can be any valid object syntax, making them much more flexible.

The format is not the only thing that has changed, however. Variables can now be passed down to the query function, not just the key like so:

const { status, data, error } = useQuery(
    // These will be used as the query key
    ['todo', todoId],
    // These will get passed directly to our query function
    [
      debug,
      {
        foo: true,
        bar: false,
      },
    ],
    fetchTodoById
  )
};

This will cause the query to be rerun if foo or bar change, but we can stop this if we want by setting a ReactQueryConfigProvider around the components that house this query with a queryFnParamsFilter to only pass in the key if we want.


Overall, while v1 will require you to update many of the places in your code where you are making API calls if you are already using react-query, it will create much more readable code thanks to the more declarative API available in the newest, and first, major version.

Discussion

markdown guide