-
-
Save rostero1/3ba3f40a63e05154c5f59b513221b00b to your computer and use it in GitHub Desktop.
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
const reducer = (state, action) => { | |
switch (action.type) { | |
case 'query': { | |
const query = action.payload; | |
if (state.queryEffectId) { | |
cancelEffect(state.queryEffectId); | |
} | |
const queryEffectId = emitEffect( | |
send => { | |
const controller = new AbortController(); | |
const signal = controller.signal; | |
abortableRequest(query, signal, randomRange(3500, 5000)) | |
.then(result => { | |
send({ type: 'result', payload: { query, result: result } }); | |
}) | |
.catch(err => { | |
if (err.name === 'AbortError') { | |
// do nothing | |
} else { | |
console.error(err); | |
} | |
}); | |
return () => { | |
controller.abort(); | |
}; | |
}, | |
{ cancelOnUnmount: true } | |
); | |
return { ...state, query: query, status: 'loading', queryEffectId }; | |
} | |
case 'result': { | |
const { result } = action.payload; | |
return { | |
...state, | |
result: result, | |
status: 'success', | |
}; | |
} | |
default: | |
throw new Error('Unexpected action'); | |
} | |
}; |
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
// 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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment