DEV Community

Jean Lambert
Jean Lambert

Posted on • Updated on

Create a custom HTTP hook with TypeScript

Hey, React community, in this article, you will find how to create a simple-to-use HTTP hook for making HTTP calls on your React or React Native app.

First, let's create a simple hook

export function useHttp<T>() {}
  • T is the type of data our hook and the HTTP response will return

Let's add some parameters

export function useHttp<T>(
  request: HttpRequest<T>,
  cb?: HttpCallback<T>,
  config?: HttpConfig<T>,
): HttpHook<T> {}
  • Request: request: HttpRequest<T>
type HttpRequest<T> = (body?: any) => Promise<T>

The HttpRequest should be the actual API call our hook will execute. The body object is the data you will pass every time you make a request.

This could be simplified by passing an URL instead of a promise, but what if you want to call multiple APIs at the same time?

  • Callback: cb?: HttpCallback<T>
type HttpCallback<T> = (data: T, error?: HttpError) => void

A callback function that's going to be called every time the API call is resolved.

  • Config: config?: HttpConfig<T>
type HttpConfig<T> = {
  headers: any,
  timeout: number
}

Here you can add any custom configuration you want to your API call, or maybe something that alters the behavior of your hook.

  • Return type
type HttpHook<T> = [(body?: any) => Promise<void>, {
  loading: boolean, 
  data: T, 
  error: HttpError | null
}]

Our hook will return 2 values

  1. The actual HTTP request
  2. An object with the loading state, the error state, and the response data

The error type could be whatever you API returns. E.g:

export class HttpError {
  status: number
  message: string

  constructor(status: number, message: string) {
    this.status = status
    this.message = message
  }
}

Now let's go with the actual logic of our hook:

// Create some states for our 
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<HttpError | null>(null)

async function fetchApi(body?: any): Promise<void> {
  setLoading(true)
  let err = null
  let data: T = {} as T
  try {
    // Await for the request
    const response: any = await request(body)
    // This might vary based on your API
    if(response.data) {
      data = response.data;
    } else {
      data = response;
    }
  } catch (_err) {
    err = _err
  } finally {
    // Set loading to false and verify the response status, if there's an error or whatever you need
    setLoading(false)
    if (err) {
      if(cb) cb(data, err)
      setError(err)
    } else {
      setResponse(data)
      setError(null)
      if(cb) cb(data)
    }
  }
}

return [fetchApi, {loading, data: response, error}]

This is the simplest logic for our hook, from this you can add your own configuration params, change the validation based on your API, you could also create a dataExtractor function if you need to use multiple APIs.

Now! Let's go with some examples.

First, create a data type and a dummy API call.

type LightsaberType = {
  color: string,
  material: string
}

let lightsabers: LightsaberType[] = [{
  color: 'blue',
  material: 'Electrum Plated'
}, {
  color: 'red',
  material: 'Durasteel'
}]

const fetchLightsaberList = async () => {
  return lightsabers
}

And it's as simple as this

export const LightsaberListScreen = () => {
  const [getLightsabers, { loading, data }] = useHttp<LightsaberType[]>(fetchLightsaberList)

  useEffect(() => {
    getLightsabers()
  }, [])

  return (
    <View>
      {loading? (
        <ActivityIndicator />
      ) : (
        <FlatList
          data={data || []}
          renderItem={({item}) => null}
        />
      )}
    </View>
  )
}

But, what if you want to handle your own state...

export const LightsaberListScreen = () => {
  const [lightsabers, setLightsabers] = useState<LightsaberType[]>([])
  const [getLightsabers, { loading }] = useHttp<LightsaberType[]>(
    fetchLightsaberList,
    (data, err) => {
      if(err) showCustomToast('Oh no, I don\'t have any lightsaber!')
      setLightsabers(data)
    }
  )

  useEffect(() => {
    getLightsabers()
  }, [])

  return (
    <View>
      {loading? (
        <ActivityIndicator />
      ) : (
        <FlatList
          data={lightsabers}
          renderItem={({item}) => null}
        />
      )}
    </View>
  )
}

OR you need to pass a body

const postLightsaber = async (lightsaber: LightsaberType) => {
  lightsabers.push(lightsaber)
  return lightsabers
}


...LightsaberListScreen component
const [createLightsaber] = useHttp<LightsaberType[]>(
    (body: LightsaberType) => postLightsaber(body),
    (data, err) => {
      if(err) showCustomToast('Oh no, you\'ll need to go back to Ilum')
      setLightsabers(data)
    }
  )

return (
  <Button 
    text="Create your own lightsaber"
    onPress={() =>  createLightsaber({
        color: 'cian',
        material:'Aurodium'
  })}/>
)

This is my first article so I really hope you like it and I'm completely open to suggestions!

Happy coding 🚀

Top comments (0)