react-use-yield

0.3.2 • Public • Published

react-use-yield

State hooks for asynchronous state management with respect of react-hooks/exhaustive-deps lint rule and AbortController functionality

Basic usage

The most basic option is useYieldState which hides some functionality under the hood

import { useYieldState } from 'react-use-yield'

function MyComponent () {
  const [startIndex, setStartIndex] = useState(0)

  const { posts, users } = useYieldState(
    // 1st argument: an async generator function that is being ran as in  
    // useEffect based on dependency array. This function receives 2 arguments:
    // getState - a function that returns current state
    // signal - passed to fetch to make it abortable when the effect clears
    async function * (getState, signal) {
      const posts = await fetchJSON(
        `${API_URL}/posts?_start=${startIndex}&_limit=3`, 
        { signal }
      )

      // call of yield causes rerender as this.setState of class component (it 
      // merges the state but only if merged state is different from previous)
      // after call of yield the state is already updated and painted to UI
      yield { posts }

      // let's say we need to ask for users mentioned in posts that we need to 
      // fetch here call of getState() returns current state after previous 
      // update
      const neededUserIds = getUniqueUserIds(getState().posts, getState().users)

      const users = await fetchJSON(
        `${API_URL}/users?ids=${neededUserIds}`, 
        { signal }
      )
      yield {
        users: [...getState().users, ...users].filter(makeUnique)
      }
    },

    // this dependency aray is honored by react-hooks/exhaustive-deps if 
    // "additionalHooks" option is configured.
    [startIndex],

    // initial state
    { posts: [], users: [] }
  )
}

useYieldState also can use a simple async function instead of async generator:

function MyComponent () {
  const [startIndex, setStartIndex] = useState(0)

  const { posts, users } = useYieldState(
    async (getState, signal) => {
      const posts = await fetchJSON(
        `${API_URL}/posts?_start=${startIndex}&_limit=3`, 
        { signal }
      )

      const neededUserIds = getUniqueUserIds(posts, getState().users)

      const users = await fetchJSON(
        `${API_URL}/users?ids=${neededUserIds}`, 
        { signal }
      )

      // this way the state is updated only once by a return
      return {
        posts,
        users: [...getState().users, ...users].filter(makeUnique)
      }
    },
    [startIndex],
    { posts: [], users: [] }
  )
}

Reducer-like usage

Let's imagine you need more than one "action" to be executed in your component, for example you have a button that resets all data to default empty state and another one that reloads the data explicitly.
In that case useYieldReducer is recommended (it's quite similar to useReducer usage):

import { useYieldReducer } from 'react-use-yield'

function MyComponent () {
  const [startIndex, setStartIndex] = useState(0)

  const [{ posts, users }, dispatch] = useYieldReducer(
    async function * (getState, signal, action) {
      // Take note that ANY execution of this function, despite the action 
      // called, will call abortController.abort() for it's previous call, 
      // so "reset" action will abort "reload" action if it hasn't beed 
      // flushed to state
      switch (action) {
        case 'reset':
          yield {
            posts: [],
            users: []
          }
          // don't forget to break if you're using async generator
          // when using async function - it's safe to just return new state
          break
        
        // we have a "reload" action that just re-runs effect explicitly
        case 'reload':
        // default usage is the case when effect is ran initialy, or by a 
        // depencency change
        default:
          const posts = await fetchJSON(
            `${API_URL}/posts?_start=${startIndex}&_limit=3`, 
            { signal }
          )
          yield { posts }

          const neededUserIds = getUniqueUserIds(
            getState().posts, 
            getState().users
          )

          const users = await fetchJSON(
            `${API_URL}/users?ids=${neededUserIds}`, 
            { signal }
          )
          yield { 
            users: [...getState().users, ...users].filter(makeUnique)
          }
      }
    },
    [startIndex],
    { posts: [], users: [] }
  )

  function handleResetClick () {
    dispatch('reset')
  }
  
  function handleReloadClick () {
    dispatch('reload')
  }
}

ESLint rules configuration

To use "react-hooks/exhaustive-deps" rule with this hook you need to install eslint-plugin-react-hooks and configure your .eslintrc in the following way:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/exhaustive-deps": ["error", {
      "additionalHooks": "(useYieldState|useYieldReducer)"
    }]
  }
}

Advanced usage

useYieldState and useYieldReducer are actually just thin wrappers over the more complex and more versatile function useYield.

Use it with care and maximize your awareness about undesired side effects

Here is an absolute equivalent of the 1st example with the usage of useYield:

import { useYield } from 'react-use-yield'

function MyComponent () {
  const [startIndex, setStartIndex] = useState(0)

  // useYield returns a tuple of state and "run" function which immediately 
  // executes a desired effect and returns an instance of abortController
  const [{ posts, users }, run] = useYield({ posts: [], users: [] })

  // a regular useEffect is used
  useEffect(() => {
    const abortController = run(async function * (getState, signal) {
      const posts = await fetchJSON(
        `${API_URL}/posts?_start=${startIndex}&_limit=3`, 
        { signal }
      )

      yield { posts }

      const neededUserIds = getUniqueUserIds(getState().posts, getState().users)

      const users = await fetchJSON(
        `${API_URL}/users?ids=${neededUserIds}`, 
        { signal }
      )
      yield {
        users: [...getState().users, ...users].filter(makeUnique)
      }
    })

    return () => abortController?.abort()
  }, [startIndex])
}
// it can also take a simple sync function.
function handleClear () {
  run(getState => ({
    posts: [],
    users: []
  }))
}

// the other obvious benefit of useYield is that you can call "run" in 
// different places of your code and execute different state updates. Just 
// keep in mind that you still need to track your abortables
const abortController = useRef(null)

const updateUsers = useCallback(() => {
  if (abortController.current && !abortController.current.signal.aborted) {
    abortController.current.abort()
  }
  abortController.current = run(async (getState, signal) => {
    const allUsersOfCurrentPage = getUniqueUserIds(getState().posts, [])
    const users = await fetchJSON(
      `${API_URL}/users?ids=${allUsersOfCurrentPage}`, 
      { signal }
    )
    return {
      users
    }
  })
}, [])

run function can also take an additional "options" argument where you can pass your own abort controller instance. This function is made just for extra convenience, it's not recommended for frequent use

useEffect(() => {
  const abortController = new AbortController()

  run(async (getState, signal) => {
    /* ... */
  }, { abortable: abortController })

  run(async (getState, signal) => {
    /* ... */
  }, { abortable: abortController })

  return () => abortController.abort()
})

Readme

Keywords

Package Sidebar

Install

npm i react-use-yield

Weekly Downloads

11

Version

0.3.2

License

MIT

Unpacked Size

42.8 kB

Total Files

15

Last publish

Collaborators

  • lerayne