A simple React hook to help you with data fetching

UFO - Use fetch orderly

A simple React hook to help you with data fetching

Fetch API and Axios help you fetching data but when you need to link the status of a request to your React state you are on your own.

Handling the UI state related to a request can be repetitive and error-prone, especially if you have to

  • handle related requests within the same component
  • ignore requests results after your component is unmounted
  • abort requests in certain conditions
  • handle race conditions

Taking advantage of React hooks react-ufo helps you deal with this complexity.

Installation

npm install --save react-ufo

import {useFetcher} from "react-ufo"

How to use

Basic usage

useFetcher handles the state of a request for you and much more.

The minimal usage of useFetcher looks like the following:

const [callback, [loading, error, data]] = useFetcher(fetcher)

A fetcher function is a normal function that fetches some data and returns a promise.

Here an example of fetcher function:

const getTodo = async (id) => {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/" + id);
  return response.json();
};

When you want your request to start, all you need to do is to invoke callback, after doing so, loading, error, and data will be updated in accordance with the status of your request.

Any argument you pass to callback will be passed to your fetcher.

Note:
Do not create a new fetcher function on every render, useFetcher will create a new callback anytime a new fetcher instance is received. In case your fetcher depends on props simply pass them to callback and your fetcher will receive them.

Here a basic example showing how to use useFetcher in an event callback such as onClick

Fetching on mount/update

By default, before a request is started, useFetcher will return loading=false, error=null, data=null.

Sometimes you might want your initial request state to be different.

One example is if you plan to request your data on the component mount/update, in this case, you might want your initial request state to have loading=true.

useFetcher can receive a second argument indicating the initial state before your request starts.

Here how you override the default loading state to be true

const [callback, [loading, error, data]] = useFetcher(fetcher, {loading:true})

Now if you want your request to start on mount all you need to do is

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

You don't have to worry about passing callback as a dependency of useEffect, callback will only change if your fetcher changes.

Fetching on mount/update with props

Sometimes a fetcher might need some data in order to retrieve data, for example, the getTodo presented earlier needs an id argument.

Assuming id is a prop of your component all you need to do is

useEffect(()=>{
  callback(id)
},[id,callback])

this ensures that your fetcher will be invoked on mount and anytime id updates, which is usually what you want.

Here a basic example showing how to use useFetcher during mount/update

Cascading fetches

Sometimes 2 requests depend on each other.

Let's say that you fetched a todo object containing a userId field and you want to use userId to fetch a user object.

Here how you can handle this use case with useFetcher


...

const [fetchTodo, [loadingTodo, todoError, todo]] = useFetcher(todoFetcher, {loading:true})
const [fetchUser, [loadingUser, userError, user]] = useFetcher(userFetcher, {loading:true})

useEffect(()=>{
  fetchTodo(todoId).then((todo)=>{
    fetchUser(todo.userId)
  })
},[todoId, fetchTodo, fetchUser])

...

Here the full example showing this use case

Ignoring a pending request

If your component is unmounted while one of its requests is still pending useFetcher will take care of ignoring its result avoiding an attempt to perform a setState on an unmounted component.

Sometimes you might want to ignore the result of a request for other reasons too.

callback.ignore() can be invoked if you need to ignore the result of a pending request.

If a pending request is marked as ignored loading, error and data will not be updated once the request is completed.

Aborting a pending request

callback.abort() can be invoked anytime you want to abort a pending request.

Unfortunately in order for callback.abort() to work properly there is some little more wiring that you'll need to do.

useFetcher will take care of passing an abort signal to your fetcher as its last argument.

In order for callback.abort() to work you'll need to pass the abort signal to your Fetch API.

Here an example showing how to enable fetch abortion on the getTodo fetcher presented earlier

const getTodo = async (id, signal) => {
  const response = await fetch("https://jsonplaceholder.typicode.com/todos/" + id, {signal});
  return response.json();
};

If your fetcher is not passing the abort signal to fetch API invoking callback.abort() will not abort the request but the request will still be marked as ignored.

If a request is marked as ignored loading, error and data will not be updated once the request is completed.

Here an example showing how to abort a request

Aborting a pending request is quite easy when using Fetch API but it can also be achieved if you are using other libraries such as axios

If you are wondering how to abort a request started by Axios instead of Fetch API you can find an example here

Keeping state between fetches

By default useFetcher erases the data of a request anytime a new one is started.

Most of the times this is what you want but there are cases where you want to keep the data visible to the user until new data are retrieved.

If you need to keep data between fetches you can simply use useState from React.

Here an example showing how to keep data while multiple request are pending:

const [data, setData] = useState()
const [callback, [loading, error, _data]] = useFetcher(fetcher)

...

const myEventCallback = ()=>{
  callback(1).then((data)=>{
    setData(data)
    callback(2).then((data)=>{
        setData(data)
    })
  })
} 

In the previous example _data is set to null anytime a new request is started while data is only valued when a request is completed.

Debouncing requests

Here an example showing one simple way to debounce requests

Mutating state

Sometimes you might want to change your request state manually.

One common scenario when this can happen is if your user decides to ignore and remove a request error message displayed on the screen.

useFetcher provides you setLoading, setError, setData and setRequestState for you to handle these use cases.

Here the full signature of useFetcher:

const [callback, [loading, error, data], setRequestState] = useFetcher(fetcher)
const [setLoading, setError, setData] = setRequestState

setLoading, setError, setData and setRequestState should be self explanatory, they work exactly like the setState in const [state, setState] = useState()

Putting all together

Here an example showing how useFetcher can be used to implement a simple CRUD application

useFetcher API

Here the full useFetcher API

const initialRequestState = {loading:false, error:null, data:null} //these are the default values if initialRequestState is not provided
const [callback, requestState, setRequestState] = useFetcher(fetcher, initialRequestState)
const [loading, error, data] = requestState
const [setLoading, setError, setData] = setRequestState

What exactly is useFetcher returning?

useFetcher returns a result object shaped as follow:

{
  callback,
  requestState: {
    loading,
    error,
    data
  },
  setRequestState: {
    setLoading,
    setError,
    setData
  }
}

result, requestState and setRequestState are also iterable, therefore, if you find it convenient for renaming, you can destructure them into an array as follow

const [callback, [loading, error, data], [setLoading, setError, setData]] = result

When destructuring into an array you obviously need to rely on the order we specified for each key, therefore, in case you don't want to extract all the fields from result, you might need to write something like the following:

const [callback, [loading, , data], [, setError]] = result

Because result is an object, accessing its fields by key (e.g const data = result.requestState.data) is going to work as expected too.

Because result is an object, doing object destructuring is going to work as expected too.

Note that even though setRequestState contains setLoading, setError, setData it is a function and can be used to update loading, error and data in a single render.

Note:
Even though result, requestState and setRequestState are iterable they are not arrays, therefore something like result[0] or result.requestState[0] is not going to work.

Examples

  1. Basic fetch in event callback
  2. Basic fetch on mount/update
  3. Aborting a pending request
  4. Handling requests depending on each others
  5. Debouncing requests
  6. Simple CRUD application
  7. Aborting a pending request started with axios

Package versioning

Breaking changes might be made between 0.x.x versions.
Starting from version 1.0.0 every breaking change will result in a major version update.
The changelog will give you details about every change between versions.

Dependencies

This package has zero dependencies but in order to support fetches abortion you will need AbortController (or a polyfill such as abortcontroller-polyfill) in your environment

GitHub