Created
February 4, 2020 01:51
-
-
Save kalda341/1bd241363d07b7956531efd877bbcbce 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
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