Module

useApiFetch

Custom hook for retrieving data from a Spark API Interface.

This hook consolidates some of the chore-work involved with getting data from an API. As well as locating the API interface and managing the data you're expecting back from the API call, you have to handle the asynchronous nature of the fetch, do something with potential errors, and make sure you can render something valid and informative for the user at every moment. useApiFetch abstracts away most of this management stuff.

Basic usage

The simplest usage looks like this:

require { useApiFetch, FetchStatus } from '@jcu/spark'

// ...

const [errors, data, status] = useApiFetch(
    'api-name', '1',
    (api) => api.getSomeData()
)

// ...

if (status === FetchStatus.INIT) { return "...preparing..."}
if (status === FetchStatus.FETCH) { return "...loading..."}
if (status === FetchStatus.ERROR) {
    console.log(errors)
    return "Oh no! Errors!"
}
if (status === FetchStatus.DATA) {
    return <InfoRenderer info={data} />
}

This usage of the hook has three arguments and creates three const variables.

The first two args are apiName and apiVersion. These are the name and version of your API Interface class, and are used by the hook to retrieve the interface from the ApiManager.

The third argument is a callback you write, that recieves an instance of your requested API interface as the first argument, and should make the api calls required to retrieve and then return whatever data you need from the API. In the example, it's calling an api interface function getSomeData() and returning whatever that returns. It's okay for this function to take some time; the hook will arrange things so that it happens in the background.

After running this code you've created three const variables in your component: errors* is an array of Error objects describing problems that occurred during the api call. If everything goes well, this is zero length. data* will eventually hold the data returned by your callback. It's initially undefined. status* tells you what status the fetch is in, out of four possible values. They're selected from the FetchStatus enum. The diagram below shows the four statuses and how your hook instance can move between them. You should use the value of status to decide what gets rendered. When status equals FetchStatus.DATA, then the data has arrived and you can show it to the user.

Status Diagram
                .--------------------.
                |       ERROR        |
                '--------------------'
                    A           | deps change OR
                    |           | user refresh
           net fail |           |
        OR bad data |           V
        .------------------------------------.
        |              FETCHING              |
        '------------------------------------'
            A                      A      | good data
            |                      |      | arrives
       deps |       deps change OR |      |
    resolve |         user refresh |      V
    .--------------.        .--------------------.
    |     init     |        |      HAS DATA      |
    '--------------'        '--------------------'

Note that it's possible to be in the error state and still have data returned (for example, it's possible to add an error from your callback, but still return data); in that case, the state will be ERROR but your data variable will still include data returned by your callback.

Use other vars in API calls

You can include other variables, e.g. useState() vars you're tracking, directly in your API callback. This adds a dependency to the hook, so you will also have to give the hook a dependency array as a fourth argument.

const [hpMovieId, setHpMovieId] = getState(1)
// ...
const [errors, hpTitle, status] = useApiFetch(
    'hp-movies', '1',
    (api) => api.getMovieInfo('title', hpMovieId),
    [hpMovieId]
)

Here the getMovieInfo() api call needs the field to get ('title'), and the movie id (initially 1). So after the api returns, we can expect data to equal "Harry Potter and the Philosopher's Stone".

Since you included hpMovieId in the dependency array, if that variable changes, the hook will automatically re-run your API fetch.

Don't fetch until everything's ready

If you need to hold off on your fetch until conditions are right, you can supply a function to say when you're ready to go. To do this provide an object as the fifth argument, with a ready key that's your function. Return false from this function if you don't want to hit the API yet.

E.g. if you start a user's movie selection as null, you will want the user to choose a movie, then you can get the title.

const [hpMovieId, setHpMovieId] = getState(null)
// ...
const [errors, hpTitle, status] = useApiFetch(
    'hp-movies', '1',
    (api) => api.getMovieInfo('title', hpMovieId),
    [hpMovieId],
    {
        ready: () => { hpMovieId !== null }
    }
)

Now, no api call happens until you set the hpMovieId to something other than null -- and then, the hook notices the change and automatically kicks off the fetch. Note that any variable you refer to inside your ready function must be listed in your dependency array, so the hook knows when to check again.

Other things you get when API-ing

Your API callback receives an API Interface instance as its first argument; you can accept a second argument to get access to some other stuff.

Also note that here, since I'm using await in my callback, I need to declare it as an async function.

const [errors, hpTitle, status] = useApiFetch(
    'hp-movies', '1',
    async (api, controls) => {
        // controls.logId is a fresh UUID you should use in your
        // `X-JCU-Log-Id` header.
        const myLogId = controls.logId

        const result = await api.getMovieInfo('title', hpMovieId, myLogId)

        // controls.addError(err) is how you signal that there
        // was a problem. Give it a string or an `Error` if have
        // one, but anything `.toString()`-ible is okay.
        if (result.serverProblemReport) {
            controls.addError(data.serverProblemReport)
        } else {
            return result
        }
    }
    [hpMovieId]
)

Force a refresh

You hardly ever need to manually invoke an API refresh; React's ability to detect dependency changes means things will refresh whenever they need to. Sometimes though you know that server data has changed, and want to imperatively invoke a new data fetch.

To do that, add a fourth element to your array of consts:

const [errors, hpTitle, status, refresh] = useApiFetch(

..then later, you can invoke refresh() to force your api fetch to happen. The hook still respects your ready function, so if your refresh isn't happening, check that you aren't passing in a ready that returns false.

View Source tools/hooks/useApiFetch.ts, line 15