0.1.3 • Published 4 years ago

uf v0.1.3

Weekly downloads
51
License
MIT
Repository
github
Last release
4 years ago

use-http logo

Features

  • SSR (server side rendering) support
  • TypeScript support
  • 2 dependencies (use-ssr, urs)
  • GraphQL support (queries + mutations)
  • Provider to set default url and options
  • Request/response interceptors !--https://github.com/ava/use-http#user-content-interceptors--
  • React Native support
  • Aborts/Cancels pending http requests when a component unmounts
  • Built in caching
  • Persistent caching support
  • Suspense support
  • Retry functionality

Usage

Examples + Videos

  • useFetch - lazy, non-lazy, both npm.io npm.io
  • useFetch - request/response interceptors npm.io npm.io
  • useFetch - retries, retryOn, retryDelay npm.io npm.io
  • useFetch - abort, timeout, onAbort, onTimeout npm.io
  • useFetch - persist, cache npm.io
  • useFetch - cacheLife, cachePolicy npm.io
  • useFetch - suspense npm.io npm.io
  • useFetch - pagination npm.io npm.io
  • useQuery - GraphQL npm.io
  • useFetch - Next.js npm.io
  • useFetch - create-react-app npm.io

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

function Todos() {
  // this will run a GET to /todos on mount, and load the
  // data into `todos`. AKA NON-LAZY
  const [todos = [], setTodos, todosAPI] = useFetch('/todos')

  const [adding, setAdding] = useState(false)
  async function addTodo() {
    setAdding(true)
    // all methods inside `todosAPI` are LAZY.
    // Aka, you must call `setTodos` to update `todos`
    // AND you must handle your own loading state
    const { data: newTodo, ok } = await todosAPI.post({ title: 'my new todo' })
    // add the newTodo to the front of the list
    if (ok) setTodos([newTodo, ...todos])
    setAdding(false)
  }

  return (
    <>
      <button onClick={addTodo}>{adding ? 'Adding Todo...' : 'Add Todo'}</button>
      {todosAPI.error && 'Error!'}
      {todosAPI.loading && 'Loading Initial Todos...'}
      {todos.map(todo => <div key={todo.id}>{todo.title}</div>}
    </>
  )
}

This fetch is run onMount/componentDidMount by default. If no method is specified, GET is the default.

import useFetch from 'uf'

function Todos() {
  const options = {} // these options accept all native `fetch` options
  const [todos = [],, todosAPI] = useFetch('/todos', options)
  return (
    <>
      {todosAPI.error && 'Error!'}
      {todosAPI.loading && 'Loading Initial Todos...'}
      {todos.map(todo => <div key={todo.id}>{todo.title}</div>}
    </>
  )
}

Can put suspense in 2 places. Either useFetch (A) or Provider (B).

import useFetch, { Provider } from 'uf'

function Todos() {
  // A. can put `suspense: true` here
  const [todos = []] = useFetch('/todos', { suspense: true })
  return todos.map(todo => <div key={todo.id}>{todo.title}</div>)
}

function App() {
  const options = {
    suspense: true // B. can put `suspense: true` here too
  }
  return (
    <Provider url='https://example.com' options={options}>
      <Suspense fallback='Loading...'>
        <Todos />
      </Suspense>
    </Provider>
  )
}

Can put suspense in 2 places. Either useFetch (A) or Provider (B).

import useFetch, { Provider } from 'uf'

function Todos() {
  const [todos = [], setTodos, todosAPI] = useFetch('/todos', {
    lazy: true,
    // A. can put `suspense: true` here
    suspense: true
  })

  const loadInitialTodos = async () => {
    const { data: todos, ok } = await todosAPI.get('/todos')
    if (ok) setTodos(todos)
  }

  // componentDidMount
  useEffect(() => { loadInitialTodos() }, [])

  return todos.map(todo => <div key={todo.id}>{todo.title}</div>)
}

function App() {
  const options = {
    suspense: true // B. can put `suspense: true` here too
  }
  return (
    <Provider url='https://example.com' options={options}>
      <Suspense fallback='Loading...'>
        <Todos />
      </Suspense>
    </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 'uf'

function Todos() {
  const perPage = 15
  const [page, setPage] = useState(1)
  // aka: load initial todos on mount into `data`. Re-runs when url changes
  const [todos = [],, todosAPI] = useFetch(`/todos?page=${page}&perPage=${perPage}`, {
    // appends newly fetched todos
    onNewData: (currTodos = [], newTodos) => [...currTodos, ...newTodos],
    perPage, // stops making more requests if last todos fetched < 15.
  })         // `perPage` REQUIRED if you want loadingMore to work properly

  return (
    <>
      {todosAPI.error && 'Error!'}
      {todos.map(todo => (
        <div key={todo.id}>{todo.title}</div>
      )}
      {!todosAPI.loading && 'Loading Initial Todos...'}
      <button onClick={() => setPage(page + 1)}>
        {todoAPI.loadingMore ? 'Loading More...' : 'Load More'}
      </button>
    </>
  )
}

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

Or if you want more control you can do

function Todos() {
  const perPage = 15
  const page = useRef(1)
  // aka: load initial todos on mount into `todos`
  const [todos = [], setTodos, todosAPI] = useFetch('/todos')

  const [loadingMore, setLoadingMore] = useState(false)
  async function loadMore() {
    const hasMore = todos.length % perPage === 0
    if (!hasMore) return
    setLoadingMore(true)
    const { data: moreTodos, ok } = await todosAPI.get(`?page=${++page.current}&perPage=${perPage}`)
    if (ok) {
      // setTodos would use the cache key `/todos` and save this into cache
      setTodos(todos => [...todos, ...moreTodos])
    }
    setLoadingMore(false)
  }

  return (
    <>
      {todosAPI.error && 'Error!'}
      {todos.map(todo => (
        <div key={todo.id}>{todo.title}</div>
      )}
      {!todosAPI.loading && 'Loading Initial Todos...'}
      <button onClick={loadMore}>{loadingMore ? 'Loading More...' : 'Load More'}</button>
    </>
  )
}

var [data, setData, api] = useFetch('/api')

// want to use object destructuring? You can do that too
var { data, setData, ...api } = useFetch('/api')

const {
  loading,  // ONLY CHANGES WHEN CALLED VIA NON-LAZY
  error,
  cache,    // methods: get, set, has, delete, clear (like `new Map()`)
  // lazy methods
  get,
  post,
  put,
  patch,
  delete    // don't destructure `delete` though, it's a keyword
  mutate,   // GraphQL
  query,    // GraphQL
  abort
} = api

var {
  data,    // the json value (or whatever responseType) of the response
  error,   // if we get a bad network call, this is the error
  path,    // in this case, would be `/api`
  // all the rest are normal fields of the JS Response class
  ...response
} = await api.get()

// var [, setTodos, todosAPI] = useFetch('/todos', { lazy: true })
const [repos = [], setRepos, reposAPI] = useFetch('https://api.github.com/search/repositories?q=', { lazy: true })

// the line below is not isomorphic, but for simplicity we're using the browsers `encodeURI`
const [searching, setSearching] = useState(false)
const searchGithubRepos = e => {
  setSearching(true)
  const { data: repos, ok } = await reposAPI.get(encodeURI(e.target.value))
  if (ok) setRepos(repos.data.items)
  setSearching(false)
}

<>
  <input onChange={searchGithubRepos} />
  <button onClick={reposAPI.abort}>Abort</button>
  {searching ? 'Searching...' : repos.map(repo => (
    <div key={repo.id}>{repo.name}</div>
  ))}
</>

const QUERY = `
  query Todos($userID string!) {
    todos(userID: $userID) {
      id
      title
    }
  }
`

function App() {
  const [user = {},, userAPI] = useFetch(QUERY, { lazy: true })

  const [loading, setLoading] = useState(false)
  const getTodosForUser = async id => {
    setLoading(true)
    await userAPI.query({ userID: id })
    setLoading(false)
  }

  return (
    <>
      <button onClick={() => getTodosForUser('theUsersID')}>Get User's Todos</button>
      {loading ? 'Loading...' : <pre>{user.name}</pre>}
    </>
  )
}

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: async ({ 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. const res = response if (res.data) res.data = toCamel(res.data) return res } } }

return ( ) }

<a target="_blank" rel="noopener noreferrer" href='https://codesandbox.io/s/usefetch-provider-requestresponse-interceptors-s1lex'><img  width='150px' height='30px' src='https://codesandbox.io/static/img/play-codesandbox.svg' /></a>  <a target="_blank" rel="noopener noreferrer" href='https://www.youtube.com/watch?v=3HauoWh0Jts&list=PLZIwrWkE9rCdUybd8t3tY-mUMvXkCdenW&index=8'><img  width='150px' height='30px' src='https://github.com/ava/use-http/raw/master/public/watch-youtube-video.png' /></a>

</details>

<details id='form-data'><summary><b>File Uploads (FormData)</b></summary>
    
This example shows how we can upload a file using `useFetch`.
    
```jsx
import useFetch from 'uf'

const FileUploader = () => {
  const [file, setFile] = useState()
  
  const { post } = useFetch('/upload')

  const uploadFile = async () => {
    const data = new FormData()
    data.append('file', file)
    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>
  )
}

import useFetch from 'uf'

const Todos = () => {
  // let's say for this request, you don't want the `Accept` header at all
  const [todos = [],, todosAPI]= useFetch('/todos', globalOptions => {
    delete globalOptions.headers.Accept
    return globalOptions
  })
  return (
    <>
      {todosAPI.error && todosAPI.error.messge}
      {todosAPI.loading && "Loading Initial Todos..."}
      {todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
    </>
  )
}

const App = () => {
  const options = {
    headers: {
      Accept: 'application/json'
    }
  }
  return (
    <Provider url='https://url.com' options={options}><Todos /></Provider>
}

In this example you can see how retryOn will retry on a status code of 305, or if we choose the retryOn() function, it returns a boolean to decide if we will retry. With retryDelay we can either have a fixed delay, or a dynamic one by using retryDelay(). Make sure retries is set to at minimum 1 otherwise it won't retry the request. If retries > 0 without retryOn then by default we always retry if there's an error or if !response.ok. If retryOn: [400] and retries > 0 then we only retry on a response status of 400.

import useFetch from 'uf'

const TodosRetry = () => {
  const [todos = []] = useFetch('https://httpbin.org/status/305', {
    // make sure `retries` is set otherwise it won't retry
    retries: 1,
    retryOn: [305],
    // OR
    retryOn: async ({ attempt, error, response }) => {
      // returns true or false to determine whether to retry
      return error || response && response.status >= 300
    },

    retryDelay: 3000,
    // OR
    retryDelay: ({ attempt, error, response }) => {
      // exponential backoff
      return Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)
      // linear backoff
      return attempt * 1000
    }
  })

  return todos.map(todo => <div key={todo.id}>{todo.title}</div>
}

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

OptionDescriptionDefault
cacheLifeAfter a successful cache update, that cache data will become stale after this duration0
cachePolicyThese will be the same ones as Apollo's fetch policies. Possible values are cache-and-network, network-only, cache-only, no-cache, cache-first. Currently only supports cache-first or no-cachecache-first
dataAllows you to set a default value for dataundefined
interceptors.requestAllows you to do something before an http request is sent out. Useful for authentication if you need to refresh tokens a lot.undefined
interceptors.responseAllows you to do something after an http response is recieved. Useful for something like camelCasing the keys of the response.undefined
loadingAllows you to set default value for loadingfalse unless the last argument of useFetch is []
onAbortRuns when the request is aborted.empty function
onErrorRuns when the request get's an error. If retrying, it is only called on the last retry attempt.empty function
onNewDataMerges the current data with the incoming data. Great for pagination.(curr, new) => new
onTimeoutCalled when the request times out.empty function
persistPersists data for the duration of cacheLife. If cacheLife is not set it defaults to 24h. Currently only available in Browser.false
perPageStops making more requests if there is no more data to fetch. (i.e. if we have 25 todos, and the perPage is 10, after fetching 2 times, we will have 20 todos. The last 5 tells us we don't have any more to fetch because it's less than 10) For pagination.0
responseTypeThis will determine how the data field is set. If you put json then it will try to parse it as JSON. If you set it as an array, it will attempt to parse the response in the order of the types you put in the array. Read about why we don't put formData in the defaults in the yellow Note part here.['json', 'text', 'blob', 'readableStream']
retriesWhen a request fails or times out, retry the request this many times. By default it will not retry.0
retryDelayYou can retry with certain intervals i.e. 30 seconds 30000 or with custom logic (i.e. to increase retry intervals).1000
retryOnYou can retry on certain http status codes or have custom logic to decide whether to retry or not via a function. Make sure retries > 0 otherwise it won't retry.[]
suspenseEnables React Suspense mode. examplefalse
timeoutThe request will be aborted/cancelled after this amount of time. This is also the interval at which retries will be made at. in milliseconds. If set to 0, it will not timeout except for browser defaults.0
const options = {
  // accepts all `fetch` options such as headers, method, etc.

  // The time in milliseconds that cache data remains fresh.
  cacheLife: 0,

  // Cache responses to improve speed and reduce amount of requests
  // Only one request to the same endpoint will be initiated unless cacheLife expires for 'cache-first'.
  cachePolicy: 'cache-first' // 'no-cache'
  
  // set's the default for the `data` field
  data: [],

  // 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: async ({ response }) => {
      // note: `response.data` is equivalent to `await response.json()`
      return response // returning the `response` is important
    }
  },

  // set's the default for `loading` field
  loading: false,
  
  // called when aborting the request
  onAbort: () => {},
  
  // runs when an error happens.
  onError: ({ error }) => {},

  // this will allow you to merge the `data` for pagination.
  onNewData: (currData, newData) => {
    return [...currData, ...newData] 
  },
  
  // called when the request times out
  onTimeout: () => {},
  
  // this will tell useFetch not to run the request if the list doesn't haveMore. (pagination)
  // i.e. if the last page fetched was < 15, don't run the request again
  perPage: 15,

  // Allows caching to persist after page refresh. Only supported in the Browser currently.
  persist: false,

  // this would basically call `await response.json()`
  // and set the `data` and `response.data` field to the output
  responseType: 'json',
  // OR can be an array. It's an array by default.
  // We will try to get the `data` by attempting to extract
  // it via these body interface methods, one by one in
  // this order. We skip `formData` because it's mostly used
  // for service workers.
  responseType: ['json', 'text', 'blob', 'arrayBuffer'],

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

  // The time between retries
  retryDelay: 10000,
  // OR
  // Can be a function which is used if we want change the time in between each retry
  retryDelay({ attempt, error, response }) {
    // exponential backoff
    return Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)
    // linear backoff
    return attempt * 1000
  },

  // make sure `retries` is set otherwise it won't retry
  // can retry on certain http status codes
  retryOn: [503],
  // OR
  async 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;
    }
  },

  // enables React Suspense mode
  suspense: true, // defaults to `false`
  
  // amount of time before the request get's canceled/aborted
  timeout: 10000,
}

useFetch(options)
// OR
<Provider options={options}><ResOfYourApp /></Provider>

Who's using useFetch?

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

Browser Support

If you need support for IE, you will need to add additional polyfills. The React docs suggest these polyfills, but from this issue we have found it to work fine with the react-app-polyfill. If you have any updates to this browser list, please submit a PR!

EdgeFirefoxChromeSafariOpera
12+last 2 versionslast 2 versionslast 2 versionslast 2 versions

Feature Requests/Ideas

If you have feature requests, submit an issue to let us know what you would like to see!

Todos

  • prefetching
  • global cache state management
  • optimistic updates
  • persist support for React Native
  • better loading state management. When using only 1 useFetch in a component and we use Promise.all([get('/todos/1'), get('/todos/2')]) then don't have a loading true, loading false on each request. Just have loading true on 1st request, and loading false on last request.
  • is making a gitpod useful here? 🤔
  • suspense
    • triggering it from outside the <Suspense /> component.
      • add .read() to request
      • or make it work with just the suspense: true option
      • both of these options need to be thought out a lot more^
    • tests for this^ (triggering outside)
    • cleanup tests in general. Snapshot tests are unpredictably not working for some reason.
  • maybe add translations like this one
  • maybe add contributors all-contributors
  • add sponsors similar to this
  • Error handling
    • if calling response.json() and there is no response yet
  • tests
    • tests for SSR
    • tests for react native see here
    • tests for GraphQL hooks useMutation + useQuery
    • tests for stale response see this PR
    • tests to make sure response.formData() and some of the other http response methods work properly
    • the onMount works properly with all variants of passing useEffect(fn, [request.get]) and not causing an infinite loop
    • async tests for interceptors.response
    • aborts fetch on unmount
    • does not abort fetch on every rerender
    • retryDelay and timeout are both set. It works, but is annoying to deal with timers in tests. resource
    • timeout with retries > 0. (also do retires > 1) Need to figure out how to advance timers properly to write this and the test above
  • take a look at how react-apollo-hooks work. Maybe ad useSubscription and const request = useFetch(); request.subscribe() or something along those lines
  • make this a github package
  • Documentation:
    • show comparison with Apollo
    • figure out a good way to show side-by-side comparisons
    • show comparison with Axios
  • potential option ideas

    const request = useFetch({
      graphql: {
        // all options can also be put in here
        // to overwrite those of `useFetch` for
        // `useMutation` and `useQuery`
      },
      // by default this is true, but if set to false
      // then we default to the responseType array of trying 'json' first, then 'text', etc.
      // hopefully I get some answers on here: https://bit.ly/3afPlJS
      responseTypeGuessing: true,
    
      // Allows you to pass in your own cache to useFetch
      // This is controversial though because `cache` is an option in the requestInit
      // and it's value is a string. See: https://developer.mozilla.org/en-US/docs/Web/API/Request/cache
      // One possible solution is to move the default `fetch`'s `cache` to `cachePolicy`.
      // I don't really like this solution though.
      // Another solution is to only allow the `cache` option with the `<Provider cache={new Map()} />`
      cache: new Map(),
      // these will be the exact same ones as Apollo's
      cachePolicy: 'cache-and-network', 'network-only', 'cache-only', 'no-cache' // 'cache-first'
      // potential idea to fetch on server instead of just having `loading` state. Not sure if this is a good idea though
      onServer: true,
      onSuccess: (/* idk what to put here */) => {},
      // 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`
      refreshWhenHidden: false,
    })

// potential for causing a rerender after clearing cache if needed request.cache.clear(true)

- [ ] potential option ideas for `GraphQL`

```jsx
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 😊

  • add React Native test suite

1: https://github.com/ava/use-http/issues/new?title=[Feature%20Request]%20YOUR_FEATURE_NAME