Intro to SWR: improving data-fetching & cache for fast user-interfaces

SWR is a React Hooks library created by Vercel that simplifies data-fetching logic in your application and makes it possible to implement caching and dependent querying. SWR, or 'stale-while-revalidate,' returns data from cache first, sends the fetch request, and finally renders up-to-date data. The package is simple, lightweight, and includes features like SSR support, pagination, scroll-position recovery, and revalidation on focus.

SWR vs Axios/Fetch API

Other popular data-fetching strategies such as Axios or Fetch API make the request and return the expected response, nothing more. SWR is a layer built on top of Fetch API that provides features like caching and pagination that would otherwise need to be managed elsewhere in the codebase. As a result, SWR provides a significant advantage for React applications to have fast, reactive user interfaces. A more appropriate library to compare with SWR would be something like react-query which supports caching and pre-fetching.

Refactoring data-fetching with SWR

Earlier this year, the engineering team at Barstool launched a feature for our internal CMS to process audio files via ACR to identify copyrighted content for podcast uploads.

The client-side of this feature involves multiple successive polling requests as the files are uploaded and processed. With polling coming as a feature out of the box in SWR, we took the opportunity to refactor the data-fetching for this feature to clean things up.

const pollMedia = async (mediaId) => {
   interval.current = setInterval(async () => {
     const response = await mediaApi.findById(mediaId)

     if (response.status === 'ready') {
       clearInterval(interval.current)
       pollCopyright(media.id, media.url)
     }
   }, pollInterval)
 }

const pollCopyright = async (mediaId, url) => {
	interval.current = setInterval(async () => {
    	const response = await podcastApi.processCopyrightClaims(mediaId, url)
        
        if (response?.status === 'ready') {
            clearInterval(interval.current)
        }
    }, 5000)
}

These functions poll the copyrights and media endpoints until the API service returns the copyrighted content for the audio file. Let's refactor with SWR.

The useSWR hook accepts a key string and a fetcher function.

const ENDPOINT_KEY = '/podcast-api/admin/media'

const fetcher = url => fetch(url).then(r => r.json())

function useEpisodeCopyrightClaims() {
 	const { data, error } = useSWR(PODCAST_MEDIA_ID, fetcher)
    
    return { data, error }
}

This is the most basic setup, but we need to implement polling and a custom fetcher that returns the data once the audio file has been processed.

const ENDPOINT_KEY = '/podcast-api/admin/media'

async function fetchEpisodeMedia(url, mediaId) {
	return await mediaApi.findById(mediaId)
}

function useEpisodeCopyrightClaims(mediaObj) {
	const { data, error } = useSWR(
    	[ENDPOINT_KEY, mediaObj.id],
    	fetchEpisodeMedia, 
        { refreshInterval: 5000 }
    )
    
    return { data, error }
}

The parameters needed for our fetcher are passed within the array argument to useSWR. To manage when to stop polling, SWR includes an onSuccess handler:

const ENDPOINT_KEY = '/podcast-api/admin/media'
const interval = 5000

async function fetchEpisodeMedia(url, mediaId) {
	return await mediaApi.findById(mediaId)
}

function useEpisodeCopyrightClaims(mediaObj) {
  const [interval, setPollingInterval] = useState(interval)

  const { data, error } = useSWR(
    [ENDPOINT_KEY, mediaObj.id], 
    fetchEpisodeMedia, 
    {
      refreshInterval: interval,
      onSuccess: (data) => data?.status === 'ready' && setPollingInterval(0)
    }
  )

  return { data, error }
}

pollingInterval  gets set to zero once the media is processed, canceling the polling for the request. The last portion of this refactor involves adding the second request for copyright claims processing. SWR supports dependent fetching which makes this process easy:

const ENDPOINT_KEY = '/podcast-api/admin/media'
const interval = 5000

async function fetchEpisodeMedia(url, mediaId) {
	return await mediaApi.findById(mediaId)
}

async function fetchEpisodeCopyrightClaims(url, mediaId, mediaUrl) {
	return await podcastApi.processCopyrightClaims(mediaId, mediaUrl)
}

function useEpisodeCopyrightClaims(mediaObj) {
	const [intervalMedia, setIntervalMedia] = useState(interval)
    const [intervalCopyright, setIntervalCopyright] = useState(interval)

    const { data: media, error: mediaError } = useSWR(
      [ENDPOINT_KEY, mediaObj.id], 
      fetchEpisodeMedia, 
      { 
        refreshInterval: intervalMedia, 
        onSuccess: (data) => data?.status === 'ready' && setIntervalMedia(0) 
      }
    )

  const { data: copyrightClaims, error: copyrightError } = useSWR(
    [ENDPOINT_KEY, media.id, media.url],
    fetchEpisodeCopyrightClaims,
    { 
      refreshInterval: intervalCopyright, 
      onSuccess: (data) => data?.status === 'ready' && setIntervalCopyright(0)
    }
  )

	return { copyrightClaims, error: mediaError || copyrightError }

}

The second request will not run until media.id is defined, which removes the complexity around handling these requests separately and allows us to list the copyright claims response as follows:

function CopyrightClaimsSummary({ mediaObj }) {
	const { copyrightClaims, error } = useEpisodeCopyrightClaims(mediaObj)
    
    if (error) return <Error>Something went wrong</Error>
    if (!copyrightClaims) return <Loading />
    
    return (
    	<ul>
        	{copyrightClaims.map((item) => (
            	<li key={item.id}>
                	{item.song.name}
                </li>
            )}
        </ul>
    )
}

We are excited about how SWR simplifies data-fetching in our codebase, with all of its useful features, especially how well it fits with Next.js. Thank you, Vercel!