// Forked version of useReducerWithEmitEffect.js, but with // 1) the ability to send from within an effect // 2) cancel an effect (with an option to cancel on unmount) // NOTE: no tests and not 100% sure of the implementation. const { useCallback, useEffect, useLayoutEffect, useReducer, useRef, } = require('react'); let globalCancelId = 0; let effectCapture = null; let cancelCapture = null; export function useReducerWithEmitEffect(reducer, initialArg, init) { let isMounted = useRef(false); let updateCounter = useRef(0); let cancelables = useRef(null); if (cancelables.current == null) { cancelables.current = Object.create(null); } // Track if isMounted useLayoutEffect(() => { isMounted.current = true; return () => { isMounted.current = false; }; }, []); let wrappedReducer = useCallback( function(oldWrappedState, action) { effectCapture = []; cancelCapture = []; try { let newState = reducer(oldWrappedState.state, action.action); let lastAppliedContiguousUpdate = oldWrappedState.lastAppliedContiguousUpdate; let effects = oldWrappedState.effects || []; let cancels = oldWrappedState.cancels || []; if (lastAppliedContiguousUpdate + 1 === action.updateCount) { lastAppliedContiguousUpdate++; effects.push(...effectCapture); cancels.push(...cancelCapture); } return { state: newState, lastAppliedContiguousUpdate, effects, cancels, }; } finally { effectCapture = null; cancelCapture = null; } }, [reducer] ); let [wrappedState, rawDispatch] = useReducer( wrappedReducer, undefined, function() { let initialState; if (init !== undefined) { initialState = init(initialArg); } else { initialState = initialArg; } return { state: initialState, lastAppliedContiguousUpdate: 0, effects: null, cancels: null, }; } ); let dispatch = useCallback(function(action) { if (isMounted.current) { updateCounter.current++; rawDispatch({ updateCount: updateCounter.current, action }); } }, []); useEffect(function() { let ignoredEffects = Object.create(null); if (wrappedState.cancels) { wrappedState.cancels.forEach(function(id) { const cancelObj = cancelables.current[id]; if (cancelObj && cancelObj.cancelFn) { cancelObj.cancelFn(); } else { ignoredEffects[id] = true; } delete cancelables.current[id]; }); } if (wrappedState.effects) { wrappedState.effects.forEach(function(eff) { // Don't run if already canceled if (!ignoredEffects[eff.id]) { const cancelFn = eff.effectFn(dispatch); if (cancelFn && typeof cancelFn === 'function') { cancelables.current[eff.id] = { cancelFn, options: eff.options }; } } }); } wrappedState.cancels = null; wrappedState.effects = null; return () => { if (!isMounted.current) { Object.keys(cancelables.current).forEach(id => { const { cancelFn, options } = cancelables.current[id]; if (options.cancelOnUnmount && cancelFn) { cancelFn(); } delete cancelables.current[id]; }); } }; }); return [wrappedState.state, dispatch]; } var defaultEmitOptions = { cancelOnUnmount: false, }; export function emitEffect(effectFn, options) { if (!effectCapture) { throw new Error( 'emitEffect can only be called from a useReducerWithEmitEffect reducer' ); } const id = globalCancelId++; effectCapture.push({ id: id, effectFn: effectFn, options: Object.assign({}, defaultEmitOptions, options), }); return id; } export function cancelEffect(id) { if (!effectCapture) { throw new Error( 'cancelEffect can only be called from a useReducerWithEmitEffect reducer' ); } cancelCapture.push(id); }