use-http

? React hook for making isomorphic http requests

Github星跟蹤圖

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

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

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

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() {
  // accepts all `fetch` options
  const options = {
    data: [],       // setting default for `data` as array instead of undefined
  }
  
  const { loading, error, data } = useFetch('https://example.com/todos', options, []) // onMount (GET by default)

  return (
    <>
      {error && 'Error!'}
      {loading && 'Loading...'}
      {data.map(todo => (
        <div key={todo.id}>{todo.title}</div>
      )}
    </>
  )
}
import useFetch, { Provider } from 'use-http'

function Todos() {
  const { loading, error, data } = useFetch({
    path: '/todos',
    data: []
  }, []) // onMount

  return (
    <>
      {error && 'Error!'}
      {loading && 'Loading...'}
      {data.map(todo => (
        <div key={todo.id}>{todo.title}</div>
      )}
    </>
  )
}

const App = () => (
  <Provider url='https://example.com'>
    <Todos />
  </Provider>
)

Edit Basic Example

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], // appends newly fetched todos
    data: []
  }, [page]) // runs onMount AND whenever the `page` updates (onUpdate)

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

Edit Basic Example

⚠️ The response object cannot be destructured! (at least not currently) ️️⚠️

var [request, response, loading, error] = useFetch('https://example.com')

// want to use object destructuring? You can do that too
var {
  request,
  // the `response` is everything you would expect to be in a normal response from an http request with the `data` field added.
  // ⚠️ The `response` object cannot be destructured! (at least not currently) ️️⚠️
  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

⚠️ 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'
})
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>
  ))}
</>

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

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

Edit Basic Example

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

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: {
      // every time we make an http request, this will run 1st before the request is made
      // url, path and route are supplied to the interceptor
      // request options can be modified and must be returned
      request: async (options, url, path, route) => {
        if (isExpired(token)) {
          token = await getNewToken()
          setToken(token)
        }
        options.headers.Authorization = `Bearer ${token}`
        return options
      },
      // every time we make an http request, before getting the response back, this will run
      response: (response) => {
        // unfortunately, because this is a JS Response object, we have to modify it directly.
        // It shouldn't have any negative affect since this is getting reset on each request.
        // use "eslint-disable-next-line" if you're getting linting errors.
        if (response.data) response.data = toCamel(response.data)
        return response
      }
    }
  }
  
  return (
    <Provider url='http://example.com' options={options}>
      <SomeComponent />
    <Provider/>
  )
}

Edit Basic Example

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

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') // will return just the user's name
    const text = await response.text()
    setName(text)
  }
  
  return (
    <>
      <button onClick={handleClick}>Load Data</button>
      {error && error.messge}
      {loading && "Loading..."}
      {name && <div>{name}</div>}
    </>
  )
}

Edit Basic Example

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 = () => {
  // let's say for this request, you don't want the `Accept` header at all
  const { loading, error, data: todos } = useFetch(globalOptions => {
    delete globalOptions.headers.Accept
    return {
      data: [],
      ...globalOptions
    }
  }, []) // onMount
  
  // can also do this and overwrite the url like this
  // const { loading, error, data: todos } = useFetch('https://my-new-url.com', 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, ```jsx
const options = {
// accepts all fetch options such as headers, method, etc.

// used to be baseUrl. You can set your URL this way instead of as the 1st argument
url: 'https://example.com',

// called when the request times out
onTimeout: () => {},

// called when aborting the request
onAbort: () => {},

// this will allow you to merge the data however you choose. Used for Pagination
onNewData: (currData, newData) => {
return [...currData, ...newData]
},

// amount of times it should retry before erroring out
retries: 3,

// amount of time before the request (or request(s) for each retry) errors out.
timeout: 10000,

// set's the default for the data field
data: [],

// set's the default for loading field
loading: false,

// typically, interceptors would be added as an option to the <Provider />
interceptors: {
request: async (options, url, path, route) => { // async is not required
return options // returning the options is important
},
response: (response) => {
return response // returning the response is important
}
}
}

useFetch(options)
// OR


Who's using use-http?
----------------------

Does your company use use-http? Consider sponsoring the project to fund new features, bug fixes, and more.

<a href="https://ava.inc" style="margin-right: 2rem;" target="_blank">
  <img width="200px" src="https://ava.inc/ava-logo-green.png" />
</a>
<a href="https://github.com/microsoft/DLWorkspace">
  <img height="200px" src="https://github.com/alex-cory/use-http/raw/master/public/microsoft-logo.png" />
</a>


Feature Requests/Ideas
----------------------
If you have feature requests, let's talk about them in [this issue](https://github.com/alex-cory/use-http/issues/13)!

Todos
------
 - [ ] maybe add translations [like this one](https://github.com/jamiebuilds/unstated-next)
 - [ ] add browser support to docs [1](https://github.com/godban/browsers-support-badges) [2](https://gist.github.com/danbovey/b468c2f810ae8efe09cb5a6fac3eaee5) (currently does not support ie 11)
 - [ ] maybe add contributors [all-contributors](https://github.com/all-contributors/all-contributors)
 - [ ] add sponsors [similar to this](https://github.com/carbon-app/carbon)
 - [ ] tests
   - [ ] tests for SSR
   - [ ] tests for FormData (can also do it for react-native at same time. [see here](https://stackoverflow.com/questions/45842088/react-native-mocking-formdata-in-unit-tests))
   - [ ] tests for GraphQL hooks `useMutation` + `useQuery`
   - [ ] tests for stale `response` see this [PR](https://github.com/alex-cory/use-http/pull/119/files)
   - [ ] tests to make sure `response.formData()` and some of the other http `response methods` work properly
   - [ ] aborts fetch on unmount
 - [ ] take a look at how [react-apollo-hooks](https://github.com/trojanowski/react-apollo-hooks) work. Maybe ad `useSubscription` and `const request = useFetch(); request.subscribe()` or something along those lines
 - [ ] make this a github package
 - [ ] Make work with React Suspense [current example WIP](https://codesandbox.io/s/7ww5950no0)
 - [ ] 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
       - [ ] figure out a good way to show side-by-side comparisonsf
     - [ ] show comparison with Axios
     - [ ] how this cancels a request on unmount of a component to avoid the error "cannot update state during a state transition" or something like that due to an incomplete http request
 - [ ] Dedupe requests done to the same endpoint. Only one request to the same endpoint will be initiated. [ref](https://www.npmjs.com/package/@bjornagh/use-fetch)
 - [ ] Cache responses to improve speed and reduce amount of requests
 - [ ] maybe add syntax for middle helpers for inline `headers` or `queries` like this:
```jsx
  const request = useFetch('https://example.com')
  
  request
    .headers({
      auth: jwt      // this would inline add the `auth` header
    })
    .query({         // might have to use .params({ }) since we're using .query() for GraphQL
      no: 'way'      // this would inline make the url: https://example.com?no=way
    })
    .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({
  onSuccess: (/* idk what to put here */) => {},
  onError: (error) => {},
  
  // can retry on certain http status codes
  retryOn: [503],
  // OR
  retryOn(attempt, error, response) {
    // retry on any network error, or 4xx or 5xx status codes
    if (error !== null, response.status >= 400) {
      console.log(`retrying, attempt number ${attempt + 1}`);
      return true;
    }
  },
  
  // This function receives a retryAttempt integer and returns the delay to apply before the next attempt in milliseconds
  retryDelay(attempt, error, response) {
    // applies exponential backoff
    return Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)
    // applies linear backoff
    return attempt * 1000
  },
  
  // this will tell useFetch not to run the request if the list doesn't haveMore
  // i.e. if the last page fetched was < 15, don't run the request again
  perPage: 15,
  
  // these will be the exact same ones as Apollo's
  // this will eventually default to 'cache-first'
  cachePolicy: 'cache-first', // 'cache-first', 'cache-and-network', 'network-only', 'cache-only', 'no-cache'
  
  // The time in milliseconds that cache data remains fresh.
  // After a successful cache update, that cache data will become stale after this duration
  cacheTime: 10000,
  
  // The time in milliseconds that unused/inactive cache data remains in memory.
  // When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration.
  invalidateCacheTime: 10000,
  
  // potential idea to fetch on server instead of just having `loading` state. Not sure if this is a good idea though
  onServer: true,
  
  // if you would prefer to pass the query in the config
  query: `some graphql query`
  
  // if you would prefer to pass the mutation in the config
  mutation: `some graphql mutation`
  
  // enabled React Suspense mode
  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
    }
  }
`
  • make code editor plugin/package/extension that adds GraphQL syntax highlighting for useQuery and useMutation ?
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>
)
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>
    </>
  )
}

主要指標

概覽
名稱與所有者ava/use-http
主編程語言TypeScript
編程語言TypeScript (語言數: 2)
平台
許可證MIT License
所有者活动
創建於2019-04-11 03:26:44
推送於2024-02-07 16:10:12
最后一次提交2023-05-04 18:14:25
發布數8
最新版本名稱1.0.8 (發布於 )
第一版名稱1.0 (發布於 )
用户参与
星數2.3k
關注者數17
派生數115
提交數714
已啟用問題?
問題數191
打開的問題數68
拉請求數155
打開的拉請求數22
關閉的拉請求數30
项目设置
已啟用Wiki?
已存檔?
是復刻?
已鎖定?
是鏡像?
是私有?