recoil-toolkit
Version:
125 lines (112 loc) • 3.87 kB
text/typescript
import { useCallback, useEffect, useRef, useState } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import type { CallbackInterface } from 'recoil';
import {
uniqueId,
hide,
show,
useRecoilCurrentSnapshot,
useRecoilCurrentGetLoadable,
} from '../_core';
import {
lastTaskByKey,
taskById,
tasks,
errorStack as errorStackAtom,
loader,
DEFAULT_LOADER,
} from '../atoms';
import type { TaskOptions, RecoilTaskInterface } from '../types';
import { TaskStatus } from '../types';
import { pushTask, updateTaskDone, updateTaskError, pushError } from '../updaters';
import { createFork } from './fork';
import { cancelTask, createCancelled, createWaitFor } from './waitFor';
export function useRecoilTask<Args extends ReadonlyArray<unknown>, Return = void, Data = unknown>(
taskCreator: (a: RecoilTaskInterface) => (...args: Args) => Return,
deps?: ReadonlyArray<unknown>,
options?: TaskOptions<Data, Args>,
) {
const optionsRef = useRef(options || {}); //options freeze
const { cancelOnUnmount, key, errorStack, loaderStack, exclusive, dataSelector, autoStart } =
optionsRef.current;
if (exclusive && !key) {
throw new Error('Exclusive tasks must have a key!');
}
const [taskId, setTaskId] = useState(0);
const task = useRecoilValue(exclusive ? lastTaskByKey(key || '') : taskById(taskId));
// eslint-disable-next-line react-hooks/rules-of-hooks
const taskData = dataSelector ? useRecoilValue(dataSelector) : task?.data;
const getSnapshot = useRecoilCurrentSnapshot();
const getLoadable = useRecoilCurrentGetLoadable();
const execute = useRecoilCallback(
({ set, snapshot, ...rest }: CallbackInterface) =>
async (...args: Args) => {
if (exclusive && key) {
const t = snapshot.getLoadable(lastTaskByKey(key)).getValue();
if (t && t.status === TaskStatus.Running) return;
}
const id = uniqueId();
setTaskId(id);
const cancelled = createCancelled(id);
const waitFor = createWaitFor(cancelled);
const fork = createFork(id, {
cancelled,
waitFor,
getSnapshot,
getLoadable,
set,
snapshot,
...rest,
});
try {
if (loaderStack)
set(loader(loaderStack === true ? DEFAULT_LOADER : loaderStack), show);
set(tasks, pushTask({ id, options, args }));
const data = await taskCreator({
fork,
cancelled,
waitFor,
getSnapshot,
getLoadable,
set,
snapshot,
...rest,
})(...args);
set(tasks, updateTaskDone({ data, id }));
} catch (error) {
set(tasks, updateTaskError({ error, id }));
if (errorStack) {
set(errorStackAtom, pushError({ key, error, taskId: id }));
}
} finally {
if (loaderStack)
set(loader(loaderStack === true ? DEFAULT_LOADER : loaderStack), hide);
}
},
deps,
);
useEffect(() => {
if (autoStart) {
execute(...autoStart);
}
}, [execute]);
useEffect(() => {
if (cancelOnUnmount && taskId) {
return () => {
cancelTask(taskId);
};
}
}, [taskId]);
const resetTask = useCallback(() => {
setTaskId(0);
}, []);
return {
loading: task?.status === TaskStatus.Running,
execute,
error: task?.error,
data: taskData as Data,
success: task?.status === TaskStatus.Done,
taskId,
resetTask,
};
}