/
SWR: Data Fetching Hooks

SWR: Data Fetching Hooks

The V2 frontend now uses SWR for data fetching, I’d recommend having a quick read through this to get a feel for what the library does. SWR allows us to create hooks to access each backend endpoint as well as adding things like caching and request deduplication.

At the time of writing, we currently have the following hooks:

  • useBuildings() to fetch data for all buildings, with the following derived hooks:

    • useBuilding(buildingId) to extract the data for a single building

  • useStatus() to fetch the status of all buildings and rooms, with the following derived hooks:

    • useBuildingStatus(buildingId) to extract the status of all rooms in a building

    • useRoomStatus(roomId) to extract the status of a single room

  • useBookings(roomId) to fetch all bookings for a room

Motivation

Every time we want to fetch data, we would usually need to do something like this

const [data, setData] = React.useState<DataType | undefined>(undefined); React.useEffect(() => { fetch(API_URL) .then(res => res.json()) .then(data => setData(data)) .catch(() => setData(undefined)); }, [])

This has a couple problems:

  • It’s a lot of boilerplate - this pattern is repeated everywhere we need to fetch data

  • It’s limited to one page - if another page wants to access the same data, we have to duplicate this code and request it separately

  • It has to be at the top level - to avoid making too many requests, we put this logic at the top level and “drill” the data down as a prop to whatever needs it

  • It runs every time the component renders - even if the data hasn’t changed (which it usually doesn’t in our app) it will make a new request

SWR solves each of these problems:

  • The boilerplate is abstracted behind the useSWR hook (or useSWRImmutable for data that doesn’t change)

  • We can reuse the hook in different components and pages, reducing code duplication and prop drilling

  • It implements caching! Data will only be requested if parameters change, otherwise we can use the cached data from the previous request.

Using SWR Hooks

An SWR hook returns an object containing two fields: data (named differently for each hook) and error. To use the hook, import it and call it like this:

import useData from "hooks/useData"; const { data, error } = useData();

Here’s what these fields mean:

  • data will contain the data we’re fetching, or undefined if it’s loading or an error occurred

  • error will be null if no error happened, or the error that happened

You can use this to add things like loading spinners or error popups, if you check things in this order:

  • If error is not NULL, an error occurred

  • Else if data is undefined, the data is still loading

  • Else, data has been fetched successfully

Making SWR Hooks

We build our hooks around the useSWR hook, which accepts a key and a fetcher function and returns an object containing data and error (among other things). We also should specify a type parameter to tell TypeScript what this hook returns.

The key is essentially the key used in SWR’s cache, typically we use the endpoint’s URL as the key. The fetcher function is a function that takes in the key and fetches (can be async) the data. This data returned must match the type parameter. Here’s an example:

const key = "https://freerooms.csesoc.app/api/buildings"; const fetcher = (url: string) => axios.get(url).then(res => res.data); const { data, error } = useSWR<BuildingsResponse>(key, fetcher);

We can also specify a list of keys - this allows us to add parameters to our requests. You can think of the array similar to a useEffect dependency array, if one value changes the data will be updated (this may be a new request, or we may hit the cache). The fetcher function then receives the keys in the list as it’s arguments. Here’s an example:

const url = "https://freerooms.csesoc.app/api/status"; const datetime = /* ... */; const filters = /* ... */; const fetcher = (url: string, datetime: Date, filters: Filters) => axios.get(url, { params: { datetime, ...filters } }).then(res => res.data); const { data, error } = useSWR<StatusResponse>([url, datetime, filters], fetcher);

Note that we can also configure to periodically refresh our data. Here’s some options:

// By default, it refreshes whenever the tab comes into focus // or the user's internet reconnects useSWR(key, fetcher); // We can also specify a refresh interval - this refreshes every 30s useSWR(key, fetcher, { refreshInterval: 30000 }); // We can also disable refreshing for immutable data we don't expect to // change over the lifetime of the app (all data we have now is immutable) // Note that this still refreshes if the key changes useSWRImmutable(key, fetcher)

We can now put this all together into our own hooks like this:

const fetcher = (url: string) => axios.get(url).then(res => res.data); const useBuildings = () => { const { data, error } = useSWRImmutable<BuildingReturnData>(API_URL + "/buildings", fetcher); return { buildings: data?.buildings, error }; } export default useBuildings;

Derived Hook

A useful thing we can do is make a derived hook which uses an SWR hook but extracts a portion of the data. For example, many components need a single building’s data but not every other building, so we can create a hook like this to abstract the process of extracting a single building’s data:

const useBuilding = (buildingId: string) => { const { buildings, error } = useBuildings(); // Error occurred while fetching all buildings if (error) { return { building: undefined, error }; } // Still loading if (!buildings) { return { building: undefined, error } } // Try find building const building = buildings.find(b => b.id === buildingId); if (!building) { return { building: undefined, error: new Error("No data for " + buildingId) }; } return { building, error }; }

 

Related content