DEV Community

Jean Lambert
Jean Lambert

Posted on • Edited 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>() {}
Enter fullscreen mode Exit fullscreen mode
  • 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> {}
Enter fullscreen mode Exit fullscreen mode
  • Request: request: HttpRequest<T>
type HttpRequest<T> = (body?: any) => Promise<T>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}]
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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}]
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}

Enter fullscreen mode Exit fullscreen mode

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>
  )
}

Enter fullscreen mode Exit fullscreen mode

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'
  })}/>
)

Enter fullscreen mode Exit fullscreen mode

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

Happy coding πŸš€

Top comments (0)