react-ufo v0.0.7
Introduction
When updating a UI with data retrieved form a remote server a lot of things can go wrong
- you will need to handle
loadinganderrorstate - you might have two or more requests depending on each others
- you might want to abort pending requests in certain conditions
- you might have to handle race conditions
At a first sight these problem seems no big deal but things get out of control quite easily.
Taking advantage of react hooks react-ufo helps you dealing with all 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
fetcherfunction on every render,useFetcherwill create a newcallbackanytime a newfetcherinstance is received. In case yourfetcherdepends on props simply pass them tocallbackand 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 ensure 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
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
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)
const [fetchUser, [loadingUser, userError, user]] = useFetcher(userFetcher)
useEffect(()=>{
fetchTodo(todoId).then((todo)=>{
fetchUser(todo.userId)
})
},[todoId, fetchTodo, fetchUser])
...Here the full example showing this use case
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] = setRequestStatesetLoading, 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:false} //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] = setRequestStateWhat 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]] = resultWhen 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]] = resultBecause 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,requestStateandsetRequestStateare iterable they are not arrays, therefore something likeresult[0]orresult.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 changes 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