1

In my app, I am using react-router v5 and react/typescript I have a component that uses the react-query and fetches some data. At the moment it only fetches the data when the component is rendered the first time, When navigating the request does not get cancelled and navigating back it does not make a new request. This component takes in an id parameter which fetches the data based on the id, so it needs to either refresh the component or maybe I need to add the method into the useEffect hook?

Routing component

import React from 'react';
import { BrowserRouter, Route, Switch} from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { RouteComponentProps } from "react-router-dom";
import Component1 from '../Component1';
import Component2 from '../Component2';


const queryClient = new QueryClient()

const Routing: React.FunctionComponent = () => {

  return (
    <QueryClientProvider client={queryClient}>
    <BrowserRouter>
        <Switch>
          <Route exact path="/" component={Component1} />
          <Route path="/details/:id" render={(props: RouteComponentProps<any>) => <Component2 {...props}/>} />

          <Route component={NotFound} />
        </Switch>
    </BrowserRouter>
    </QueryClientProvider>
  )
}

export default Routing;

Component2 (id)

import React from 'react';
import { useQuery } from 'react-query';
import { RouteComponentProps, useLocation } from "react-router-dom";


interface stateType {
    model: { pathname: string },
    start: { pathname: string | Date }
}

const Component2: React.FunctionComponent<RouteComponentProps<any>> = (props) => {

    const { state } = useLocation<stateType>();
    let alertInnerId = props.match.params.id;

    const fetchChart = async () => {
        const res = await fetch(`/detail/${id}`);
        return res.json();
    };

    const { data, status } = useQuery('planeInfo', fetchPlane, {
        staleTime: 5000,
    });

    return (
        <>
                {status === 'error' && (
                    <div className="mt-5">Error fetching data!</div>
                )}
                {status === 'loading' && (
                    <div className="mt-5">Loading data ...
                    </div>
                )}
                {status === 'success' && (
                   {data.map(inner => {
                       return (
                           <p>{inner.id}</p>
                       )
                   })}
                )}
            </div>
        </>
    )
}

export default Component2;

In the Component 1 I am programmatically navigating:

onClick={() => history.push(`/detail/${id}}`, { model: plane.model, start: formattedStartDateTime })}> 

Either way by programmatically or normal, its still the same.

Ahmet Emre Kilinc
  • 5,489
  • 12
  • 30
  • 42
Sole
  • 3,100
  • 14
  • 58
  • 112
  • React router will NOT cancel the network requests on its own. If you want to handle cancelling of API requests, [see this](https://stackoverflow.com/questions/31061838/how-do-i-cancel-an-http-fetch-request). You can cancel your request in the cleanup function of `useEffect` – Apoorv Kansal Oct 20 '21 at 18:16
  • But is it better to wrap the react-query in the uesEffect() – Sole Oct 20 '21 at 18:27

1 Answers1

1

[...] and navigating back it does not make a new request.

First of all, according to your code, as per the staleTime option that is set as an option on useQuery itself, the cache should invalidate every five seconds. So each time the useQuery hook is mounted (such as on route change), if five seconds have passed, a new request should be made. Your code does appear to be incomplete though as you're referencing id which appears to be undefined.

In any case, since you are requesting details of a resource with an ID, you should consider using a query key like: [planeInfo, id] instead of planeInfo alone. From the documentation:

Since query keys uniquely describe the data they are fetching, they should include any variables you use in your query function that change. For example:

function Todos({ todoId }) {
    const result = useQuery(['todos', todoId], () => 
    fetchTodoById(todoId))
}

To handle canceling the request on navigation:

You can't wrap the useQuery hook from React Query in a useEffect hook, but rather you can use use the return function of useEffect to clean up your useQuery request, effectively canceling the request when the component unmounts. With useQuery there are two ways (possibly more) to cancel a request:

  • use the remove method exposed on the returned object of useQuery
  • use the QueryClient method: cancelQueries

(see: useQuery reference here)

see: QueryClient reference here and specifically cancelQueries

Using remove with useEffect

(I've only kept the relevant bits of your code)

const Component2: React.FunctionComponent <RouteComponentProps<any>> = (props) => {
        const fetchChart = async() => {
          const res = await fetch(`/detail/${id}`);
          return res.json();
        };

        const {
          data,
          status,
          /** access the remove method **/
          remove
        } = useQuery('planeInfo', fetchPlane, {
          staleTime: 5000,
        });

        useEffect(() => {
          /** when this component unmounts, call it **/
          return () => remove()

          /** make sure to set an empty deps array **/
        }, [])

        /** the rest of your component **/

}

Calling remove like this will cancel any ongoing request, but as its name suggests, it also removes the query from the cache. Depending on whether you need to keep the data in cache or not, this may or may not be a viable strategy. If you need to keep the data, you can instead use the canceQueries method.

Using cancelQueries with useEffect

Much like before except here you need to export your queryClient instance from the routing component file (as you have it defined there) and then you're importing that instance of QueryClient into Component2 and calling cancelQueries on the cache key from useEffect:

import { queryClient } from "./routing-component"

const Component2: React.FunctionComponent <RouteComponentProps<any>> = (props) => {
        const fetchChart = async() => {
          const res = await fetch(`/detail/${id}`);
          return res.json();
        };

        const {
          data,
          status,
        } = useQuery(['planeInfo', id], fetchPlane, {
          staleTime: 5000,
        });

        useEffect(() => {
          /** when this component unmounts, call it **/
          return () => queryClient.cancelQueries(['planeInfo', id], {exact: true, fetching: true})
        }, [])

        /** the rest of your component **/
}

Here you see that I've implemented the query key as I suggested before, with the id as well. You can see why having a more precise reference to the cached object can be beneficial. I'm also using two query filters: exact and fetching. Setting exact to true will make sure React Query doesn't use pattern matching and cancel a broader set of queries. You can decide whether or not this is necessary for your implementation needs. Setting fetching to true will make sure React Query includes and cancels and queries that are currently fetching data.

Just note that by depending on useEffect, it is in some cases possible for it's parent component to unmount due to factors other than the user navigating away from the page (such as a modal). In such cases, you should move your useQuery up in the component tree into a component that will only unmount when a user navigates, and then pass the result of useQuery into the child component as props, to avoid premature cancellations.

Alternatively you could use Axios instead of fetch. With Axios you can cancel a request using a global cancel token, and combine executing that cancellation with React Router's useLocation (example here). You could of course also combine useLocation listening to route changes with QueryClient.cancelQueries. There are in fact, many possible approaches to your question.

Bryan M
  • 11
  • 2