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 const
s:
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.