Skip to content

Instantly share code, notes, and snippets.

@TarVK
Last active May 13, 2020 16:52
Show Gist options
  • Save TarVK/f9cf6ce23a411ae63a85a6b2121b76d2 to your computer and use it in GitHub Desktop.
Save TarVK/f9cf6ce23a411ae63a85a6b2121b76d2 to your computer and use it in GitHub Desktop.
React reducer hook that emulates redux's async action creator capabilities
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];
};
@TarVK
Copy link
Author

TarVK commented May 13, 2020

Usage

This useAsyncReducer is a dropin replacement for react's useReducer. And has the following signature.

const [state, dispatch, isInitializing, initPromise] = useAsyncReducer(reducer, initialArg, init);

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.

await dispatch(async (dispatch, getState)=>{
   dispatch({someAction: 3}); // May be an async action itself
   await doSmthAsync();
   const newState = getState();
   if (newState.smth) dispatch({potatoes: 3});
});

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:

const createFetchAction = ()=>{
    return async (dispatch)=>{
        const data = await getMyData();
        dispatch({data});
    }
}
const createFetchIfNeededAction = ()=>{
    return async (dispatch, getState)=>{
        const state = getState();
        if (!state.data) await dispatch(createFetchAction());
    }
}
const reducer = (state, action)=>({...state, ...action});
...
const [state, dispatch] = useReducer(reducer, {});
useEffect(()=>dispatch(createFetchIfNeededAction()), []);

Action lists

You can also dispatch a list of actions to be dispatched in sequence.
E.G.

dispatch([{someAction: 3}, {potatoes: 3}]);

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.

const [state, dispatch, isInitializing, initPromise] = useAsyncReducer(reducer, {}, async (dispatch)=>{
    const data = await getSomeData();
    dispatch({data});
});
useEffect(()=>{
    initPromise.then(()=>console.log("initializedWithData"));
}, [initPromise]);

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment