Skip to content

Instantly share code, notes, and snippets.

@kalda341
Created February 4, 2020 01:51
Show Gist options
  • Save kalda341/1bd241363d07b7956531efd877bbcbce to your computer and use it in GitHub Desktop.
Save kalda341/1bd241363d07b7956531efd877bbcbce to your computer and use it in GitHub Desktop.
import { useEffect, useRef, useReducer } from 'react';
import { merge, forEach, reject, append } from 'ramda';
export const useIsMounted = () => {
const ref = useRef(null);
useEffect(() => {
ref.current = true;
return () => {
ref.current = false;
};
});
return () => ref.current;
};
const loadingReducer = (state, action) => {
switch (action.type) {
case 'LOADING':
return merge(defaultLoadingReducerValue, {
isLoading: true,
message: action.message
});
case 'ERROR':
return merge(defaultLoadingReducerValue, {
isError: true,
error: action.error,
message: action.message
});
case 'SUCCESS':
return merge(defaultLoadingReducerValue, {
isSuccess: true,
data: action.data,
message: action.message
});
default:
throw new Error('Invalid action');
}
};
const defaultLoadingReducerValue = Object.freeze({
isLoading: false,
isError: false,
isSuccess: false,
error: null,
data: null,
message: null
});
export const useLoadingReducer = defaultValueOverrides => {
const getIsMounted = useIsMounted();
const [state, dispatch] = useReducer(
loadingReducer,
merge(defaultLoadingReducerValue, defaultValueOverrides || {})
);
const conditionalDispatch = (...args) => getIsMounted() && dispatch(...args);
return [state, conditionalDispatch];
};
const Task = (func, args) => {
const taskState = {
isRunning: false,
isCancelled: false,
isFinished: false,
isError: false,
result: null,
error: null
};
let resolve = null;
let reject = null;
const promise = new Promise((innerResolve, innerReject) => {
resolve = innerResolve;
reject = innerReject;
});
const start = () => {
const success = result => {
taskState.isRunning = false;
taskState.isCancelled = false;
taskState.isSuccess = true;
taskState.isError = false;
taskState.result = result;
taskState.error = null;
resolve(result);
};
const error = error => {
taskState.isRunning = false;
taskState.isCancelled = false;
taskState.isSuccess = false;
taskState.isError = true;
taskState.result = null;
taskState.error = error;
reject(error);
};
const cancelled = () => {
// The state should already be set by the cancel function
reject(new Error('Task was cancelled'));
};
// Immediately invoked function
(async () => {
// Handle cancel before starting generator
if (taskState.isCancelled) {
return cancelled();
}
const generator = func(...args);
let currentValue = null;
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const intermediateResult = generator.next(currentValue);
currentValue = await intermediateResult.value;
// Handle cancel check after any successful async operation
if (taskState.isCancelled) {
return cancelled();
} else if (intermediateResult.done) {
return success(currentValue);
}
} catch (e) {
// Handle cancel check after any unsuccessful async operation
if (taskState.isCancelled) {
return cancelled(e);
}
// Let the task function handle the error if it can
try {
generator.throw(e);
} catch (e) {
return error(e);
}
}
}
})();
return promise;
};
const cancel = () => {
if (!taskState.isFinished && !taskState.isError) {
taskState.isRunning = false;
taskState.isCancelled = true;
taskState.isSuccess = false;
taskState.isError = false;
taskState.result = null;
taskState.error = null;
}
};
// Actions
taskState.start = start;
taskState.cancel = cancel;
// Promise functions
taskState.catch = promise.catch.bind(promise);
taskState.then = promise.then.bind(promise);
taskState.finally = promise.finally.bind(promise);
return taskState;
};
const TaskFactory = (func, strategy) => {
let queuedTasks = [];
let runningTasks = [];
let isRunning = false;
const setRunningTasks = tasks => {
runningTasks = tasks;
isRunning = runningTasks.length !== 0;
};
const queueTask = task => {
queuedTasks = append(task, queuedTasks);
};
const maybeStartQueuedTask = () => {
if (queuedTasks.length && (strategy !== 'ENQUEUE' || !isRunning)) {
const task = queuedTasks[0];
// Note - compare by refrence
queuedTasks = reject(t => t === task, queuedTasks);
setRunningTasks(append(task, runningTasks));
task.start();
}
};
const taskFinished = task => {
// Note - compare by refrence
setRunningTasks(reject(t => t === task, runningTasks));
maybeStartQueuedTask();
};
const perform = (...args) => performFunc(func, args);
const performFunc = (func, args) => {
const task = Task(func, args);
if (strategy === 'RESTARTABLE') {
cancelAll();
} else if (strategy === 'DROP' && isRunning) {
task.cancel();
return task;
}
queueTask(task);
maybeStartQueuedTask();
task
// Prevent uncaught promise error
.catch(() => {})
.finally(() => {
taskFinished(task);
});
return task;
};
const cancelAll = () => {
queuedTasks = [];
forEach(task => task.cancel(), runningTasks);
};
return {
perform,
performFunc,
cancelAll
};
};
export const useTask = (func, strategy) => {
const factoryRef = useRef(TaskFactory(null, strategy));
const factory = factoryRef.current;
// Cancel all on unmount
useEffect(() => {
return () => {
factory.cancelAll();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Override perform to always use the most recent function
return merge(factory, {
perform: (...args) => factory.performFunc(func, args)
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment