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 buildinguseRoomStatus(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 (oruseSWRImmutable
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, orundefined
if it’s loading or an error occurrederror
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 occurredElse if
data
is undefined, the data is still loadingElse,
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 };
}