Last active
May 13, 2020 16:52
-
-
Save TarVK/f9cf6ce23a411ae63a85a6b2121b76d2 to your computer and use it in GitHub Desktop.
React reducer hook that emulates redux's async action creator capabilities
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useRef } from 'react'; | |
/** | |
* A reducer hook that allows for redux's async action creator pattern; | |
* https://redux.js.org/advanced/async-actions#async-action-creators | |
* Also allows for sequential dispatching of multiple actions if an array is passed, | |
* Is otherwise the same as the normal useReducer hook | |
* @param {Function} reducer The reducer to ue | |
* @param {*} initialState The intiial state | |
* @param {object[]} actions The initial actions to dispatch | |
* @returns {[state, dispatch, initializing, initPromise]} The resulting state and dispatch function | |
*/ | |
export const useAsyncReducer = (reducer, initialState, ...actions) => { | |
let ref; | |
// Create a way of forcing an update | |
const [, setState] = useState(1); | |
const forceUpdate = () => ref.current && setState(v => v + 1); | |
// Create a ref to store our state and dispatcher, and initialize it | |
ref = useRef(); | |
if (!ref.current || ref.current.reducer !== reducer) { | |
// Create the initial state | |
let state = initialState; | |
// Create the dispatcher that alters the state and forces updates | |
const dispatch = async action => { | |
// If the action is an array of actions, dispatch them in sequence | |
if (action instanceof Array) { | |
if (!action[0]) return []; | |
const result = await dispatch(action[0]); | |
return [result, ...(await dispatch(action.slice(1)))]; | |
} | |
// If the action is an action creator, call it | |
if (action instanceof Function) { | |
const result = action(dispatch, () => state); | |
// Return a promise indicating whether the whole action was dispatched | |
if (result instanceof Promise) return result; | |
return Promise.resolve(result); | |
} | |
// If the action is a regular action, obtain the new state and force an update | |
state = reducer(state, action); | |
forceUpdate(); | |
// Return a promise for consistency (dispatch immediately resolved) | |
return Promise.resolve(); | |
}; | |
// Execute the initial actions | |
let initPromise = Promise.resolve(); | |
if (actions.length > 0) { | |
initPromise = dispatch(actions); | |
initPromise.then(() => { | |
// Indicate that initializing is done | |
ref.current.initializing = false; | |
forceUpdate(); | |
}); | |
} | |
// Return the state getter and the dispatcher | |
ref.current = { getState: () => state, dispatch, reducer, initializing: actions.length > 0, initPromise }; | |
} | |
// Return the state and dispatcher | |
return [ref.current.getState(), ref.current.dispatch, ref.current.initializing, ref.current.initPromise]; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
This useAsyncReducer is a dropin replacement for react's useReducer. And has the following signature.
It does add some very useful additional features however
Async actions
Asynchronous actions can be dispatched similar to those in redux: https://redux.js.org/advanced/async-actions#async-action-creators
E.G.
Dispatch accepts functions to be dispatched, and calls them with the dispatch function itself, and a getter for the current state. In addition, dispatch itself returns a promise, which is resolved when the action completes executing.
Another example of how this can be used:
Action lists
You can also dispatch a list of actions to be dispatched in sequence.
E.G.
The dispatch will return a promise that resolves when all actions are dispatched, and one action will only be dispatched when the previous has finished. For synchronous actions that's not important of course, but this can be very handy for async actions.
Async init actions
You can pass any actions to the reducer to be executed on initialization. Since these actions can now be async, a state to represent whether these actions are still executing is provided as extra return values.
E.G.
Breaking changes:
For the most part, this is a dropin replacement for useReducer, there are two exceptions however. In the past, dispatching a function would simply pass said function to the reducer, and the same applies for arrays. This is no longer the case, instead the special dispatch behavior is triggered by this. If your reducer relies on dispatching arrays or functions already, it won't behave the same when starting to use useAsyncReducer.