11

I have a React application which uses SWR + Axios for data fetching (https://swr.vercel.app/docs/data-fetching). The issue is that my custom hook which uses useSwr is fetching all data initially whenever the hook is initialized. My goal is to fetch only when I call mutate. Currently the initial fetch is happening even without calling mutate. Any suggestion on how to achieve my goal here?

My application is wrapped in SWRConfig:

    <SWRConfig
      value={{
        fetcher,
      }}
    >
      <App/>
    </SWRConfig>

The fetcher is described as so:

const dataFetch = (url) => axios.get(url).then((res) => res.data);

function fetcher(...urls: string[]) {
  if (urls.length > 1) {
    return Promise.all(urls.map(dataFetch));
  }
  return dataFetch(urls);
}

My custom hook using useSwr

import useSWR, { useSWRConfig } from "swr";

export function useCars(registrationPlates: number[]): ICars {
  const { mutate } = useSWRConfig();
  const { data: carData} = useSWR<Car[]>(
    carsToFetchUrls(registrationPlates),  // returns string array with urls to fetch
    {
      revalidateOnFocus: false,    
      revalidateOnMount: false,
      revalidateOnReconnect: false,
      refreshWhenOffline: false,
      refreshWhenHidden: false,
      refreshInterval: 0,
    }
  );

  const getCar = (
    carRegistrationPlate: number,
  ): Car => {
    console.log(carData) // carData contains data from fetch even before calling mutate()

    void mutate();
    
    ...
}

Usage: (this will be located in some component that wants to use the useCars hook)

const { getCar } = useCars(carsRegistrationPlates);
juliomalves
  • 42,130
  • 20
  • 150
  • 146
DLO
  • 914
  • 1
  • 13
  • 30
  • "Currently the initial fetch is happening even without calling mutate" @Deeple mutate is typically used when you save or edit & then you want to update what is being shown to the user to reflect the said save or edit & you then call mutate to sync your cached data (with what is available on the server) which then forces swr to refetch data from the server. When swr re-fetches the data then the local cache gets updated and consequently what get shown to the user is also updated i.e. UI is refreshed. – Sangeet Agarwal Dec 07 '21 at 13:16

3 Answers3

9

You can use conditional fetching in the useSWR call to prevent it from making a request.

From useSWR Conditional Fetching docs:

Use null or pass a function as key to conditionally fetch data. If the function throws or returns a falsy value, SWR will not start the request.

export function useCars(registrationPlates: number[], shouldFetch): ICars {
    const { data: carData} = useSWR<Car[]>(
        shouldFetch ? carsToFetchUrls(registrationPlates) : null,
        { // Options here }
    );
    // ...
    return { carData, /**/ }
}

You can then use it as follows to avoid making the initial request.

const [shouldFetch, setShouldFetch] = useState(false);
const { carData } = useCars(carsRegistrationPlates, shouldFetch);

Then, when you want the make the request simply set shouldFetch to true.

setShouldFetch(true)
juliomalves
  • 42,130
  • 20
  • 150
  • 146
  • Thanks for you answer. That would indeed prevent the prefetch. But when calling `getCars` which contains `mutate` the useSwr hook would still fetch null. – DLO Dec 07 '21 at 07:58
  • Not if you explicitly call mutate with the `key` value in it, i.e. `mutate(carsToFetchUrls(registrationPlates))`. – juliomalves Dec 07 '21 at 08:21
  • Maybe I'm not using the `mutate` correctly. I've tried to `mutate(carsToFetchUrls(registrationPlates))` but `carData` is still undefined. I tried to use `async/await` for the fetch as well, but it doesn't change anything... – DLO Dec 07 '21 at 11:55
  • 1
    @Deeple Right, in this case you wouldn't use `mutate`, instead move the `shouldFetch` variable into state and then set it to `true` when you want the mutation to occur. – juliomalves Dec 07 '21 at 12:18
2

Here's a possible way of implementing what you are hoping to achieve. I've used a similar approach in one of my production app. Start by creating a custom swr hook as so

const useCars = (registrationPlates: number[]) => {
  const fetcher = (_: string) => {
    console.log("swr-key=", _);
    return dataFetch(registrationPlates);
  };

  const { data, error, isValidating, revalidate, mutate } = useSWR(`api/car/registration/${JSON.stringify(registrationPlates)}`, fetcher, {
    revalidateOnFocus: false,
  });

  return {
    data,
    error,
    isLoading: !data && !error,
    isValidating,
    revalidate,
    mutate,
  };
};

export { useCars };

Now, you can call this hook from any other component as

const { data, error, isLoading, isValidating, revalidate, mutate } = useCars(carsRegistrationPlates);

You now control what you want returned by what you pass to useCars above. Notice what is passed to the first argument to useSwr in our custom swr hook, this is the key swr uses to cache values and if this remains unchanged then swr will transparently returned the cached value.

Also, with this custom hook you are getting states such as loading, error etc. so you can take appropriate action for each of these states in your consuming component.

Sangeet Agarwal
  • 1,674
  • 16
  • 25
  • Thanks for your answer. This like the setup that I have. The useSwr hook is prefetching the data even before mutate is called. So If I just const up useCars somewhere it will fetch. In my case I have a large amount of data coming in from that fetch so I would only want it to do so when I'm calling `mutate`. Apparently `revalidateOnX: false` doesn't work for me. – DLO Dec 07 '21 at 07:51
  • Hi @Deeple, facing the same issue. Were you able to find a solution for that? – Arun Selva Kumar Jan 19 '22 at 00:48
0

You can do that by passing revalidate options with useSwr hook as follwoing:

useSWR(key, fetcher, {
  revalidateIfStale: false,
  revalidateOnFocus: false,
  revalidateOnReconnect: false
})

or using useSWRImmutable as follows:

// equivalent to
useSWRImmutable(key, fetcher);

see disable revalidation on their docs.