react-use-yield v0.3.2
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()
})