🐶 React hook for making isomorphic http requests
Main Documentation
Main Documentation
npm i use-http
- SSR (server side rendering) support
- TypeScript support
- 1 dependency (use-ssr)
- GraphQL support (queries + mutations)
- Provider to set default
url
andoptions
⚠️ Examples click me
Basic Usage (managed state) useFetch
import useFetch from 'use-http'
function Todos() {
const [todos, setTodos] = useState([])
const [request, response] = useFetch('https://example.com')
// componentDidMount
const mounted = useRef(false)
useEffect(() => {
if (!mounted.current) {
initializeTodos()
mounted.current= true
}
})
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.length > 0 && todos.map(todo => (
<div key={todo.id}>{todo.title}</div>
)}
</>
)
}
Basic Usage (no managed state) useFetch
import useFetch from 'use-http'
function Todos() {
const options = { // accepts all `fetch` options
onMount: true, // will fire on componentDidMount (GET by default)
data: [] // setting default for `data` as array instead of undefined
}
const { loading, error, data } = useFetch('https://example.com/todos', options)
return (
<>
{error && 'Error!'}
{loading && 'Loading...'}
{!loading && data.map(todo => (
<div key={todo.id}>{todo.title}</div>
)}
</>
)
}
Destructured useFetch
var [request, response, loading, error] = useFetch('https://example.com')
// want to use object destructuring? You can do that too
var {
request,
response,
loading,
error,
data,
get,
post,
put,
patch,
delete // don't destructure `delete` though, it's a keyword
del, // <- that's why we have this (del). or use `request.delete`
mutate, // GraphQL
query, // GraphQL
abort
} = useFetch('https://example.com')
var {
loading,
error,
data,
get,
post,
put,
patch,
delete // don't destructure `delete` though, it's a keyword
del, // <- that's why we have this (del). or use `request.delete`
mutate, // GraphQL
query, // GraphQL
abort
} = request
var {
data,
ok,
headers,
...restOfHttpResponse // everything you would get in a response from an http request
} = response
Relative routes useFetch
baseUrl
is no longer supported, it is now only url
var request = useFetch({ url: 'https://example.com' })
// OR
var request = useFetch('https://example.com')
request.post('/todos', {
no: 'way'
})
Abort useFetch
const githubRepos = useFetch({
url: `https://api.github.com/search/repositories?q=`
})
// the line below is not isomorphic, but for simplicity we're using the browsers `encodeURI`
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
import { useQuery } from 'use-http'
export default function QueryComponent() {
// can also do it this way:
// const [data, loading, error, query] = useQuery`
// or this way:
// const { data, loading, error, query } = useQuery`
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>}
</>
)
}
import { useMutation } from 'use-http'
export default function MutationComponent() {
const [todoTitle, setTodoTitle] = useState('')
// can also do it this way:
// const request = useMutation`
// or this way:
// const { data, loading, error, mutate } = useMutation`
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>}
</>
)
}
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/>
)
}
Hook | Description |
---|---|
useFetch |
The base hook |
useQuery |
For making a GraphQL query |
useMutation |
For making a GraphQL mutation |
This is exactly what you would pass to the normal js fetch
, with a little extra.
Option | Description | Default |
---|---|---|
onMount |
Once the component mounts, the http request will run immediately | false |
url |
Allows you to set a base path so relative paths can be used for each request :) | empty string |
data |
Allows you to set a default value for data |
undefined |
loading |
Allows you to set default value for loading |
false unless onMount === true |
useFetch({
// accepts all `fetch` options such as headers, method, etc.
url: 'https://example.com', // used to be `baseUrl`
onMount: true,
data: [], // default for `data` field
loading: false, // default for `loading` field
})
If you have feature requests, let's talk about them in this issue!
- tests
- tests for SSR
- tests for FormData (can also do it for react-native at same time. see here)
- tests for GraphQL hooks
useMutation
+useQuery
- react native support
- documentation for FormData
- Make work with React Suspense current example WIP
- get it all working on a SSR codesandbox, this way we can have api to call locally
- make GraphQL work with React Suspense
- make GraphQL examples in codesandbox
- Documentation:
- show comparison with Apollo
- Interceptors (potential syntax example) this shows how to get access tokens on each request if an access token or refresh token is expired
const App = () => {
const { get } = useFetch('https://example.com')
const [accessToken, setAccessToken] = useLocalStorage('access-token')
const [refreshToken, setRefreshToken] = useLocalStorage('refresh-token')
const { history } = useReactRouter()
const options = {
interceptors: {
async request(opts) {
let headers = {}
// refresh token expires in 1 day, used to get access token
if (!refreshToken || isExpired(refreshToken)) {
return history.push('/login')
}
// access token expires every 15 minutes, use refresh token to get new access token
if (!accessToken || isExpired(accessToken)) {
const access = await get(`/access-token?refreshToken=${refreshToken}`)
setAccessToken(access)
headers = {
Authorization: `Bearer ${access}`,
}
}
const finalOptions = {
...opts,
headers: {
...opts.headers,
...headers,
},
}
return finalOptions
},
},
headers: {
Authorization: `Bearer ${accessToken}`
}
}
return (
<Provider url='https://example.com' options={options}>
<App />
</Provider>
)
}
- Dedupe requests done to the same endpoint. Only one request to the same endpoint will be initiated. ref
- Cache responses to improve speed and reduce amount of requests
- maybe add syntax for inline headers like this
const user = useFetch()
user
.headers({
auth: jwt
})
.get()
- maybe add snake_case -> camelCase option to
<Provider />
. This would convert all the keys in the response to camelCase. Not exactly sure how this syntax should look because what if you want to have this only go 1 layer deep into the response object. Or if this is just out of scope for this library.
<Provider responseKeys={{ case: 'camel' }}><App /></Provider>
- potential option ideas
const request = useFetch({
onUpdate: [props.id] // everytime props.id is updated, it will re-run the request GET in this case
path: '/todos' // this would allow you to POST and GET to the same path onMount and on demand if you had a url in context
retry: 3, // amount of times it should retry before erroring out
retryDuration: 1000, // amount of time for each retry before timing out?
timeout: 10000, // amount of time period before erroring out
onServer: true, // potential idea to fetch on server instead of just having `loading` state. Not sure if this is a good idea though
interceptors: {
request(opts) {} // i.e. if you need to do some kind of authentication before a request
response(opts) {} // i.e. if you want to camelCase all fields in a response everytime
}
})
- add callback to completely overwrite options. 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 youruseFetch
at all for one instance in your app. This would allow you to remove that
const request = useFetch('https://url.com', globalOptions => {
delete globalOptions.headers.Authorization
return globalOptions
})
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('')
// if there's no <Provider /> used, useMutation works this way
const mutation = useMutation('http://example.com', `
mutation CreateTodo($todoTitle string) {
todo(title: $todoTitle) {
id
title
}
}
`)
// ideally, I think it should be mutation.write({ todoTitle }) since mutation ~= POST
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>
</>
)
}