useFetch
Features
- SSR (server side rendering) support
- TypeScript support
- 1 dependency (use-ssr)
- GraphQL support (queries + mutations)
- Provider to set default
url
and options
- Request/response interceptors
- React Native support
- Aborts/Cancels pending http requests when a component unmounts
Usage
Examples
Basic Usage (managed state) useFetch
If the last argument of useFetch
is not a dependency array []
, then it will not fire until you call one of the http methods like get
, post
, etc.
import useFetch from 'use-http'
function Todos() {
const [todos, setTodos] = useState([])
const [request, response] = useFetch('https://example.com')
const mounted = useRef(false)
useEffect(() => {
if (mounted.current) return
mounted.current= true
initializeTodos()
})
async function initializeTodos() {
const initialTodos = await request.get('/todos')
if (response.ok) setTodos(initialTodos)
}
async function addTodo() {
const newTodo = await request.post('/todos', {
title: 'no way',
})
if (response.ok) setTodos([...todos, newTodo])
}
return (
<>
<button onClick={addTodo}>Add Todo</button>
{request.error && 'Error!'}
{request.loading && 'Loading...'}
{todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
)}
</>
)
}
Basic Usage (no managed state) useFetch
This fetch is run onMount/componentDidMount
. The last argument []
means it will run onMount
. If you pass it a variable like [someVariable]
, it will run onMount
and again whenever someVariable
changes values (aka onUpdate
). If no method is specified, GET is the default
import useFetch from 'use-http'
function Todos() {
const options = {
data: [],
}
const { loading, error, data } = useFetch('https://example.com/todos', options, [])
return (
<>
{error && 'Error!'}
{loading && 'Loading...'}
{data.map(todo => (
<div key={todo.id}>{todo.title}</div>
)}
</>
)
}
Basic Usage (no managed state) with Provider
import useFetch, { Provider } from 'use-http'
function Todos() {
const { loading, error, data } = useFetch({
path: '/todos',
data: []
}, [])
return (
<>
{error && 'Error!'}
{loading && 'Loading...'}
{data.map(todo => (
<div key={todo.id}>{todo.title}</div>
)}
</>
)
}
const App = () => (
<Provider url='https://example.com'>
<Todos />
</Provider>
)

Pagination + Provider
The onNewData
will take the current data, and the newly fetched data, and allow you to merge the two however you choose. In the example below, we are appending the new todos to the end of the current todos.
import useFetch, { Provider } from 'use-http'
const Todos = () => {
const [page, setPage] = useState(1)
const { data, loading } = useFetch({
path: `/todos?page=${page}&amountPerPage=15`,
onNewData: (currTodos, newTodos) => [...currTodos, ...newTodos],
data: []
}, [page])
return (
<ul>
{data.map(todo => <li key={todo.id}>{todo.title}</li>}
{loading && 'Loading...'}
{!loading && (
<button onClick={() => setPage(page + 1)}>Load More Todos</button>
)}
</ul>
)
}
const App = () => (
<Provider url='https://example.com'>
<Todos />
</Provider>
)

Destructured useFetch
â ď¸ The response
object cannot be destructured! (at least not currently) ď¸ď¸â ď¸
var [request, response, loading, error] = useFetch('https://example.com')
var {
request,
response,
loading,
error,
data,
get,
post,
put,
patch,
delete
del,
mutate,
query,
abort
} = useFetch('https://example.com')
var {
loading,
error,
data,
get,
post,
put,
patch,
delete
del,
mutate,
query,
abort
} = request
Relative routes useFetch
â ď¸ baseUrl
is no longer supported, it is now only url
var request = useFetch({ url: 'https://example.com' })
var request = useFetch('https://example.com')
request.post('/todos', {
no: 'way'
})
Abort useFetch
const githubRepos = useFetch({
url: `https://api.github.com/search/repositories?q=`
})
const searchGithubRepos = e => githubRepos.get(encodeURI(e.target.value))
<>
<input onChange={searchGithubRepos} />
<button onClick={githubRepos.abort}>Abort</button>
{githubRepos.loading ? 'Loading...' : githubRepos.data.items.map(repo => (
<div key={repo.id}>{repo.name}</div>
))}
</>
GraphQL Query useFetch
const QUERY = `
query Todos($userID string!) {
todos(userID: $userID) {
id
title
}
}
`
function App() {
const request = useFetch('http://example.com')
const getTodosForUser = id => request.query(QUERY, { userID: id })
return (
<>
<button onClick={() => getTodosForUser('theUsersID')}>Get User's Todos</button>
{request.loading ? 'Loading...' : <pre>{request.data}</pre>}
</>
)
}
GraphQL Mutation useFetch
The Provider
allows us to set a default url
, options
(such as headers) and so on.
const MUTATION = `
mutation CreateTodo($todoTitle string) {
todo(title: $todoTitle) {
id
title
}
}
`
function App() {
const [todoTitle, setTodoTitle] = useState('')
const request = useFetch('http://example.com')
const createtodo = () => request.mutate(MUTATION, { todoTitle })
return (
<>
<input onChange={e => setTodoTitle(e.target.value)} />
<button onClick={createTodo}>Create Todo</button>
{request.loading ? 'Loading...' : <pre>{request.data}</pre>}
</>
)
}
Provider
using the GraphQL useMutation
and useQuery
Query for todos
import { useQuery } from 'use-http'
export default function QueryComponent() {
const request = useQuery`
query Todos($userID string!) {
todos(userID: $userID) {
id
title
}
}
`
const getTodosForUser = id => request.query({ userID: id })
return (
<>
<button onClick={() => getTodosForUser('theUsersID')}>Get User's Todos</button>
{request.loading ? 'Loading...' : <pre>{request.data}</pre>}
</>
)
}

Add a new todo
import { useMutation } from 'use-http'
export default function MutationComponent() {
const [todoTitle, setTodoTitle] = useState('')
const [data, loading, error, mutate] = useMutation`
mutation CreateTodo($todoTitle string) {
todo(title: $todoTitle) {
id
title
}
}
`
const createTodo = () => mutate({ todoTitle })
return (
<>
<input onChange={e => setTodoTitle(e.target.value)} />
<button onClick={createTodo}>Create Todo</button>
{loading ? 'Loading...' : <pre>{data}</pre>}
</>
)
}
Adding the Provider
These props are defaults used in every request inside the <Provider />
. They can be overwritten individually
import { Provider } from 'use-http'
import QueryComponent from './QueryComponent'
import MutationComponent from './MutationComponent'
function App() {
const options = {
headers: {
Authorization: 'Bearer YOUR_TOKEN_HERE'
}
}
return (
<Provider url='http://example.com' options={options}>
<QueryComponent />
<MutationComponent />
<Provider/>
)
}
Request/Response Interceptors
This example shows how we can do authentication in the request
interceptor and how we can camelCase the results in the response
interceptor
import { Provider } from 'use-http'
import { toCamel } from 'convert-keys'
function App() {
let [token, setToken] = useLocalStorage('token')
const options = {
interceptors: {
request: async (options) => {
if (isExpired(token)) {
token = await getNewToken()
setToken(token)
}
options.headers.Authorization = `Bearer ${token}`
return options
},
response: (response) => {
if (response.data) response.data = toCamel(response.data)
return response
}
}
}
return (
<Provider url='http://example.com' options={options}>
<SomeComponent />
<Provider/>
)
}

File Uploads (FormData)
This example shows how we can upload a file using useFetch
.
import useFetch from 'use-http'
const FileUploader = () => {
const [file, setFile] = useState()
const { post } = useFetch('https://example.com/upload')
const uploadFile = async () => {
const data = new FormData()
data.append('file', file)
if (file instanceof FormData) await post(data)
}
return (
<div>
{/* Drop a file onto the input below */}
<input onChange={e => setFile(e.target.files[0])} />
<button onClick={uploadFile}>Upload</button>
</div>
)
}
Handling Different Response Types
This example shows how we can get .json()
, .text()
, .formData()
, .blob()
, .arrayBuffer()
, and all the other http response methods. By default, useFetch
1st tries to call response.json()
under the hood, if that fails it's backup is response.text()
. If that fails, then you need a different response type which is where this comes in.
import useFetch from 'use-http'
const App = () => {
const [name, setName] = useState('')
const { get, loading, error, response } = useFetch('http://example.com')
const handleClick = async () => {
await get('/users/1?name=true')
const text = await response.text()
setName(text)
}
return (
<>
<button onClick={handleClick}>Load Data</button>
{error && error.messge}
{loading && "Loading..."}
{name && <div>{name}</div>}
</>
)
}

Overwrite/Remove Options/Headers Set in Provider
This example shows how to remove a header all together. Let's say you have <Provider url='url.com' options={{ headers: { Authentication: 'Bearer MY_TOKEN' } }}><App /></Provider>
, but for one api call, you don't want that header in your useFetch
at all for one instance in your app. This would allow you to remove that.
import useFetch from 'use-http'
const Todos = () => {
const { loading, error, data: todos } = useFetch(globalOptions => {
delete globalOptions.headers.Accept
return {
data: [],
...globalOptions
}
}, [])
return (
<>
{error && error.messge}
{loading && "Loading..."}
{todos && <ul>{todos.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul>}
</>
)
}
const App = () => {
const options = {
headers: {
Accept: 'application/json'
}
}
return (
<Provider url='https://url.com' options={options}><Todos /></Provider>
}
Overview
Hooks
Hook | Description |
---|
useFetch | The base hook |
useQuery | For making a GraphQL query |
useMutation | For making a GraphQL mutation |
Options
This is exactly what you would pass to the normal js fetch
, with a little extra. All these options can be passed to the <Provider options={/* every option below */} />
, or directly to useFetch
. If you have both in the <Provider />
and in useFetch
, the useFetch
options will overwrite the ones from the <Provider />
Option | Description | Default |
---|
url | Allows you to set a base path so relative paths can be used for each request :) | empty string |
onNewData | Merges the current data with the incoming data. Great for pagination. | (curr, new) => new |
onAbort | Runs when the request is aborted. | empty function |
onTimeout | Called when the request times out. | empty function |
retries | When a request fails or times out, retry the request this many times. By default it will not retry. | 0 |
timeout | The request will be aborted/cancelled after this amount of time. This is also the interval at which retries will be made at. in milliseconds | 30000 (30 seconds) |
data | Allows you to set a default value for data | undefined |
loading | Allows you to set default value for loading | false unless the last argument of useFetch is [] |
interceptors.request | Allows you to do something before an http request is sent out. Useful for authentication if you need to refresh tokens a lot. | undefined |
interceptors.response | Allows you to do something after an http response is recieved. Useful for something like camelCasing the keys of the response. | undefined |
const options = {
url: 'https://example.com',
onTimeout: () => {},
onAbort: () => {},
onNewData: (currData, newData) => {
return [...currData, ...newData]
},
retries: 3,
timeout: 10000,
data: [],
loading: false,
interceptors: {
request: async (options) => {
return options
},
response: (response) => {
return response
}
}
}
useFetch(options)
<Provider options={options}><ResOfYourApp /></Provider>
Who's using use-http?
Does your company use use-http? Consider sponsoring the project to fund new features, bug fixes, and more.
Feature Requests/Ideas
If you have feature requests, let's talk about them in this issue!
Todos
const request = useFetch('https://example.com')
request
.headers({
auth: jwt
})
.query({
no: 'way'
})
.get()
<Provider responseKeys={{ case: 'camel' }}><App /></Provider>
const request = useFetch({
onSuccess: () => {},
onError: (error) => {},
retryOn: [503],
retryOn(attempt, error, response) {
if (error !== null || response.status >= 400) {
console.log(`retrying, attempt number ${attempt + 1}`);
return true;
}
},
retryDelay(attempt, error, response) {
return Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)
return attempt * 1000
},
cachePolicy: 'cache-first',
cacheTime: 10000,
invalidateCacheTime: 10000,
onServer: true,
query: `some graphql query`
mutation: `some graphql mutation`
suspense: false,
retryOnError: false,
refreshWhenHidden: false,
})
const request = useQuery({ onMount: true })`your graphql query`
const request = useFetch(...)
const userID = 'some-user-uuid'
const res = await request.query({ userID })`
query Todos($userID string!) {
todos(userID: $userID) {
id
title
}
}
`
The Goal With Suspense (not implemented yet)
import React, { Suspense, unstable_ConcurrentMode as ConcurrentMode, useEffect } from 'react'
function WithSuspense() {
const suspense = useFetch('https://example.com')
useEffect(() => {
suspense.read()
}, [])
if (!suspense.data) return null
return <pre>{suspense.data}</pre>
}
function App() (
<ConcurrentMode>
<Suspense fallback="Loading...">
<WithSuspense />
</Suspense>
</ConcurrentMode>
)
GraphQL with Suspense (not implemented yet)
const App = () => {
const [todoTitle, setTodoTitle] = useState('')
const mutation = useMutation('http://example.com', `
mutation CreateTodo($todoTitle string) {
todo(title: $todoTitle) {
id
title
}
}
`)
const createTodo = () => mutation.read({ todoTitle })
if (!request.data) return null
return (
<>
<input onChange={e => setTodoTitle(e.target.value)} />
<button onClick={createTodo}>Create Todo</button>
<pre>{mutation.data}</pre>
</>
)
}