0.3.2 • Published 2 years ago

react-use-yield v0.3.2

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

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 useYieldSingle which hides some functionality under the hood

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

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

  const { posts, users } = useYieldSingle(
    // 1st argument: an async generator function that is being ran as in useEffect based on 
    // dependency array. This function receives 2 arguments
    async function * (getState, signal) {
      // signal is passed to fetch to make it abortable when the function is ran again
      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: [] }
  )
}

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

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

  const { posts, users } = useYieldSingle(
    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 this kind of usage is recommended (it's quite similar to useReducer usage):

function MyComponent () {
  const [startIndex, setStartIndex] = useState(0)
  const [action, setAction] = useState('init')

  const { posts, users } = useYieldSingle(
    async function * (getState, signal) {
      switch (getState().action) {
        case 'init':
        case 'reload':
          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)
          }
          break

        case 'reset':
          yield {
            posts: [],
            users: []
          }
          break

        default:
          throw new Error('unknown action')
      }
    },
    [startIndex, action],
    { posts: [], users: [] }
  )

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

Advanced usage

useYieldSingle is actually a wrapper over the more complex and more versatile function useYield.

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()
})
0.4.0-rc1

2 years ago

0.3.2

2 years ago

0.3.1

2 years ago

0.3.0

2 years ago

0.3.0-rc4

2 years ago

0.3.0-rc3

2 years ago

0.3.0-rc2

2 years ago

0.3.0-rc1

2 years ago

0.2.0

2 years ago

0.1.1

2 years ago

0.1.1-rc2

2 years ago

0.1.1-rc1

2 years ago

0.1.0

2 years ago