Skip to content

Instantly share code, notes, and snippets.

@rostero1
Forked from sophiebits/useReducerWithEmitEffect.js
Last active October 27, 2019 23:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rostero1/3ba3f40a63e05154c5f59b513221b00b to your computer and use it in GitHub Desktop.
Save rostero1/3ba3f40a63e05154c5f59b513221b00b to your computer and use it in GitHub Desktop.
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');
}
};
// 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