/use-http

🐶 React hook for making isomorphic http requests

Primary LanguageTypeScriptMIT LicenseMIT


useFetch


undefined

🐶 React hook for making isomorphic http requests
Main Documentation


npm i use-http


Features

  • SSR (server side rendering) support
  • TypeScript support
  • 1 dependency (use-ssr)
  • GraphQL support (queries + mutations)
  • Provider to set default url and options

Usage

⚠️ 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
Query for todos
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>}
    </>
  )
}
Add a new todo
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>}
    </>
  )
}
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/>
  )
}

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.

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
})

Feature Requests/Ideas

If you have feature requests, let's talk about them in this issue!

Todos

  • 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 your useFetch 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>
    </>
  )
}