Using AbortController (with React Hooks and TypeScript) to cancel window.fetch requests

bil

Bilawal Hameed

Posted on March 27, 2019

Using AbortController (with React Hooks and TypeScript) to cancel window.fetch requests

Originally posted on bilaw.al/abortcontroller.html

I have longed for being able to cancel window.fetch requests in JavaScript. It is something particularly useful, especially to adhere to React's Lifecycle, and even more so with the introduction of React Hooks.

Thankfully, we have something called AbortController!

const abortController = new AbortController()

const promise = window
  .fetch('https://api.example.com/v1/me', {
    headers: {Authorization: `Bearer [my access token]`},
    method: 'GET',
    mode: 'cors',
    signal: abortController.signal,
  })
  .then(res => res.json())
  .then(res => {
    console.log(res.me)
  })
  .catch(err => {
    console.error('Request failed', err)
  })

// Cancel the request if it takes more than 5 seconds
setTimeout(() => abortController.abort(), 5000)
Enter fullscreen mode Exit fullscreen mode

As you should expect, this will cancel the request after 5 seconds. Pretty cool, eh?

In our catch, it will give us an AbortError error with the message The user aborted a request. so we could even rewrite our error checking to consider this:

promise.catch(err => {
  if (err.name === 'AbortError') {
    console.error('Request took more than 5 seconds. Automatically cancelled.')
    return
  }

  // It wasn't that the request took longer than 5 seconds.
  console.error(err.message)
})
Enter fullscreen mode Exit fullscreen mode

React Hooks?

Of course, let's dive into that. This is something along the lines of what you'll want:

// src/hooks/useProfileInformation.jsx
import {useState, useEffect} from 'react'

export function useProfileInformation({accessToken}) {
  const [profileInfo, setProfileInfo] = useState(null)

  useEffect(() => {
    const abortController = new AbortController()

    window
      .fetch('https://api.example.com/v1/me', {
        headers: {Authorization: `Bearer ${accessToken}`},
        method: 'GET',
        mode: 'cors',
        signal: abortController.signal,
      })
      .then(res => res.json())
      .then(res => setProfileInfo(res.profileInfo))

    return function cancel() {
      abortController.abort()
    }
  }, [accessToken])

  return profileInfo
}

// src/app.jsx
import React from 'react'
import {useProfileInformation} from './hooks/useProfileInformation'

export function App({accessToken}) {
  try {
    const profileInfo = useProfileInformation({accessToken})

    if (profileInfo) {
      return <h1>Hey, ${profileInfo.name}!</h1>
    } else {
      return <h1>Loading Profile Information</h1>
    }
  } catch (err) {
    return <h1>Failed to load profile. Error: {err.message}</h1>
  }
}
Enter fullscreen mode Exit fullscreen mode

React Hooks and TypeScript?

Oh, you! Just take it already.

// src/hooks/useProfileInformation.tsx
import {useState, useEffect} from 'react'

export interface ProfileRequestProps {
  accessToken: string
}

export interface ProfileInformation {
  id: number
  firstName: string
  lastName: string
  state: 'free' | 'premium'
  country: {
    locale: string
  }
}

export function useProfileInformation({accessToken}: ProfileRequestProps): ProfileInformation | null {
  const [profileInfo, setProfileInfo] = useState(null)

  useEffect(() => {
    const abortController = new AbortController()

    window
      .fetch('https://api.example.com/v1/me', {
        headers: {Authorization: `Bearer ${accessToken}`},
        method: 'GET',
        mode: 'cors',
        signal: abortController.signal,
      })
      .then((res: Response) => res.json())
      .then((resProfileInfo: ProfileInformation) => setProfileInfo(resProfileInfo))

    return function cancel() {
      abortController.abort()
    }
  }, [accessToken])

  return profileInfo
}

// src/app.tsx
import React, { ReactNode } from 'react'
import {useProfileInformation, ProfileRequestProps, ProfileInformation} from './hooks/useProfileInformation'

export function App({accessToken}: ProfileRequestProps) : ReactNode {
  try {
    const profileInfo: ProfileInformation = useProfileInformation({accessToken})

    if (profileInfo) {
      return <h1>Hey, ${profileInfo.name}!</h1>
    } else {
      return <h1>Loading Profile Information</h1>
    }
  } catch (err) {
    return <h1>Failed to load profile. Error: {err.message}</h1>
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing

It's supported in jest and jsdom by default, so you're all set. Something like?

// src/utils.js
export const getProfileInformation = () => {
  const abortController = new AbortController()
  const response = window
    .fetch('https://api.example.com/v1/me', {signal: abortController.signal})
    .then(res => res.json())
  return {response, abortController}
}

// src/__tests__/utils.test.js
import utils from '../utils'

describe('Get Profile Information', () => {
  it('raises an error if we abort our fetch', () => {
    expect(() => {
      const profile = getProfileInformation()
      profile.abortController.abort()
    }).toThrowError()
  })
})
Enter fullscreen mode Exit fullscreen mode

Promises

Want to see how you'd use AbortController for Promises? Check out make-abortable written by fellow colleague Josef Blake

Gotchas?

Gotcha #1: No support of destructuring

Sadly, we cannot destruct our new AbortController() as such:

const {signal, abort} = new AbortController()

window
  .fetch('https://api.example.com/v1/me', {signal})
  .then(res => res.json())
  .then(res => console.log(res))

setTimeout(() => abort(), 5000)
Enter fullscreen mode Exit fullscreen mode

When we invoke the abort() method, it invokes an Uncaught TypeError: Illegal invocation error when because it is a prototype implementation that depends on this.

Conclusions

I have read up on AbortController a while ago, but glad that I have finally had a chance to fully check it out. It is impressively supported across all browsers (except Safari, unsurprisingly) so you should be able to use it in your projects :)

💖 💪 🙅 🚩
bil
Bilawal Hameed

Posted on March 27, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related