Revalidating data using React Router and the new 3.9 preloadQuery

Hi folks, looking for some help with using the new preload query features and react router.

I’m building an inventory management app, and after adding or deleting a product, returning to the All Products page doesn’t display the newly added/ deleted product.

i’m using the new createBrowserRouter (v 6.4 of React router) and the affected Route looks like so:

{
        path: '/products',
        element: <ProductPage />,
        loader: allProductsLoader,
        children: [
          {
            path: '/products/:productID',
            element: <Product />,
            loader: findProductLoader,
          },
          {
            path: '/products/add-product',
            element: <AddProduct />,
          },
        ],
      },

the allProduct loader looks like so:

export const allProductsLoader = async () => {
  return preloadQuery(ALL_PRODUCTS, {
    fetchPolicy: 'network-only',
    errorPolicy: 'all',
  })
}

addProduct action looks like so:

export const addComponent = async (name: string, cost: number, stock: number) => {
  try {
    const { data } = await client.mutate({
      mutation: ADD_COMPONENT,
      variables: { name, cost, stock },
    })

    return data.addComponent
  } catch (error: unknown) {
    return error as Error
  }
}

and finally the product page looks like so:

const ProductPage: React.FC = () => {
  const queryRef = useLoaderData()
  const { data: loaderData, error } = useReadQuery(queryRef as QueryRef<loaderData>)

  if (error) {
    notify({ error: error.message })
    return <div>Can't display page</div>
  }

  return (
    <div className="container mx-auto px-4 py-8">
      <ProductList products={loaderData.allProducts} />
    </div>
  )
}

(ProductList is just a TanStack Table displaying the product data)

Something worth mentioning though, ProductList displays the AddProduct page in a modal like so:

 {location.pathname != '/products' && location.pathname != '/' && (
        <Modal onClose={() => navigate('/products')}>
          <Outlet />
        </Modal>
      )}

so I’m technically never leaving the /products url, when pressing addProduct i go to /products/:productId and after it is successfully added, I navigate back to /products.

I’ve tried a bunch of approaches like navigating to the same page again, useRevalidator, etc… to force a refetch but nothing short of refreshing the page or going to a different page and returning works.

Thanks in advance.

Hey @vkats90 :wave:

Since you’re not leaving the URL, my guess is that your allProductsLoader never re-reruns, so that network-only fetch policy has no effect after the mutation. Because of this, your component is relying on cache updates in order to rerender with new data.

Since you’re adding an item to an array, my suspicion is that the cache isn’t updated after the mutation so your component never rerenders as a result. In other words, I don’t see an update option passed to client.mutate to do a manual cache update or a refetchQueries option to trigger a refetch of your query after the mutation. Because of this, the mutation will finish, but it doesn’t know what to do with the object that returned from your mutation because the cache doesn’t know what array to add it to or in what order it should add it. You’ll need to make sure the cache is updated with the new item in order for this to work.

Check out the doc for mutations which shows an example of how to add an item to a list after a mutation. I recommend trying to update the cache manually to avoid the network request, but doing a refetch via refetchQueries should work here as well.

Hope this helps!

Thanks for your reply Jerel.

I think I did try the update cache approach but it didn’t work for me, I might have done something wrong though so I’d have to give it another go. I did find a solution that worked thankfully, after many days trying different approaches, I used the useOutletContext to pass on the state of allComponents to it’s children and update it this way. I then changed the fetch policy to “cache and network” so that next time I navigate out and back in the list gets updated through fetching. Works nicely.

A few other features I would like to implement but have no Idea how to are:

  • error case in the loader function, before the page loads (The preloader returns a promise, and it only resolves after navigating to the page so I’m not sure if it’s possible?)
  • Polling to the server to refetch the newest data once in a while.

For the cache updates, I’d recommend installing Apollo Client Devtools if you don’t already have them installed. This extension will let you explore the cache so you can see if your manual cache updates are working the way you expect it to.

Glad you found a working solution otherwise though!

error case in the loader function, before the page loads (The preloader returns a promise, and it only resolves after navigating to the page so I’m not sure if it’s possible?)

If I’m interpreting what you’re saying here, are you looking to prevent the route from transitioning until the query has loaded? If so, preloadQuery returns a queryRef which has a toPromise method on it.

// note the addition of the `.toPromise()` call at the end of `preloadQuery`
export const allProductsLoader = async () => {
  return preloadQuery(ALL_PRODUCTS, {
    fetchPolicy: 'network-only',
    errorPolicy: 'all',
  }).toPromise()
}

You can use this to delay the route transition until the promise has settled. That promise will reject if the query contains errors and you’re using the default errorPolicy of none. This should cause React Router to render the error component for that route.

See more in the Suspense docs about toPromise if you’re curious more about it.

Polling to the server to refetch the newest data once in a while.

Unfortunately we don’t have polling built into our suspense hooks. Polling is tricky when it comes to React Suspense and transitions and we think the behavior might be confusing, so we’ve opted to avoid native polling for support in our suspense hooks for now. You can implement your own polling using a combination of refetch and your own timer. You can get access to the refetch function when using query preloading with the useQueryRefHandlers hook.

Hope this helps!