ra-core
Version:
Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React
243 lines • 11.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useMutationWithMutationMode = void 0;
const react_1 = require("react");
const react_query_1 = require("@tanstack/react-query");
const useAddUndoableMutation_1 = require("./undo/useAddUndoableMutation.cjs");
const util_1 = require("../util/index.cjs");
const useMutationWithMutationMode = (params = {}, options) => {
const queryClient = (0, react_query_1.useQueryClient)();
const addUndoableMutation = (0, useAddUndoableMutation_1.useAddUndoableMutation)();
const { mutationKey, mutationMode = 'pessimistic', mutationFn, getMutateWithMiddlewares, updateCache, getQueryKeys, onUndo, ...mutationOptions } = options;
if (mutationFn == null) {
throw new Error('useMutationWithMutationMode mutation requires a mutationFn');
}
const mutationFnEvent = (0, util_1.useEvent)(mutationFn);
const updateCacheEvent = (0, util_1.useEvent)(updateCache);
const getQueryKeysEvent = (0, util_1.useEvent)(getQueryKeys);
const getSnapshotEvent = (0, util_1.useEvent)(
/**
* Snapshot the previous values via queryClient.getQueriesData()
*
* The snapshotData ref will contain an array of tuples [query key, associated data]
*
* @example
* [
* [['posts', 'getList'], { data: [{ id: 1, title: 'Hello' }], total: 1 }],
* [['posts', 'getMany'], [{ id: 1, title: 'Hello' }]],
* ]
*
* @see https://tanstack.com/query/v5/docs/react/reference/QueryClient#queryclientgetqueriesdata
*/
(queryKeys) => queryKeys.reduce((prev, queryKey) => prev.concat(queryClient.getQueriesData({ queryKey })), []));
const onUndoEvent = (0, util_1.useEvent)(onUndo ?? noop);
const getMutateWithMiddlewaresEvent = (0, util_1.useEvent)(getMutateWithMiddlewares ??
noop);
const mode = (0, react_1.useRef)(mutationMode);
(0, react_1.useEffect)(() => {
mode.current = mutationMode;
}, [mutationMode]);
// This ref won't be updated when params change in an effect, only when the mutate callback is called (See L247)
// This ensures that for undoable and optimistic mutations, the params are not changed by side effects (unselectAll for instance)
// _after_ the mutate function has been called, while keeping the ability to change declaration time params _until_ the mutation is called.
const paramsRef = (0, react_1.useRef)(params);
// Ref that stores the snapshot of the state before the mutation to allow reverting it
const snapshot = (0, react_1.useRef)([]);
// Ref that stores the mutation with middlewares to avoid losing them if the calling component is unmounted
const mutateWithMiddlewares = (0, react_1.useRef)(mutationFnEvent);
// We need to store the call-time onError and onSettled in refs to be able to call them in the useMutation hook even
// when the calling component is unmounted
const callTimeOnError = (0, react_1.useRef)();
const callTimeOnSettled = (0, react_1.useRef)();
// We don't need to keep a ref on the onSuccess callback as we call it ourselves for optimistic and
// undoable mutations. There is a limitation though: if one of the side effects applied by the onSuccess callback
// unmounts the component that called the useUpdate hook (redirect for instance), it must be the last one applied,
// otherwise the other side effects may not applied.
const hasCallTimeOnSuccess = (0, react_1.useRef)(false);
const mutation = (0, react_query_1.useMutation)({
mutationKey,
mutationFn: async (params) => {
if (params == null) {
throw new Error('useMutationWithMutationMode mutation requires parameters');
}
return (mutateWithMiddlewares
.current(params)
// Middlewares expect the data property of the dataProvider response
.then(({ data }) => data));
},
...mutationOptions,
onMutate: async (...args) => {
if (mutationOptions.onMutate) {
const userContext = (await mutationOptions.onMutate(...args)) || {};
return {
snapshot: snapshot.current,
// @ts-ignore
...userContext,
};
}
else {
// Return a context object with the snapshot value
return { snapshot: snapshot.current };
}
},
onError: (...args) => {
if (mode.current === 'optimistic' ||
mode.current === 'undoable') {
const [, , onMutateResult] = args;
// If the mutation fails, use the context returned from onMutate to rollback
onMutateResult.snapshot.forEach(([key, value]) => {
queryClient.setQueryData(key, value);
});
}
if (callTimeOnError.current) {
return callTimeOnError.current(...args);
}
if (mutationOptions.onError) {
return mutationOptions.onError(...args);
}
// call-time error callback is executed by react-query
},
onSuccess: (...args) => {
if (mode.current === 'pessimistic') {
const [data, variables] = args;
// update the getOne and getList query cache with the new result
updateCacheEvent({ ...paramsRef.current, ...variables }, {
mutationMode: mode.current,
}, data);
if (mutationOptions.onSuccess &&
!hasCallTimeOnSuccess.current) {
mutationOptions.onSuccess(...args);
}
}
},
onSettled: (...args) => {
if (mode.current === 'optimistic' ||
mode.current === 'undoable') {
const [, , variables] = args;
// Always refetch after error or success:
getQueryKeysEvent({ ...paramsRef.current, ...variables }, {
mutationMode: mode.current,
}).forEach(queryKey => {
queryClient.invalidateQueries({ queryKey });
});
}
if (callTimeOnSettled.current) {
return callTimeOnSettled.current(...args);
}
if (mutationOptions.onSettled) {
return mutationOptions.onSettled(...args);
}
},
});
const mutate = async (callTimeParams = {}, callTimeOptions = {}) => {
const { mutationMode, returnPromise = mutationOptions.returnPromise, onError, onSettled, onSuccess, ...otherCallTimeOptions } = callTimeOptions;
// store the hook time params *at the moment of the call*
// because they may change afterwards, which would break the undoable mode
// as the previousData would be overwritten by the optimistic update
paramsRef.current = params;
// Store the mutation with middlewares to avoid losing them if the calling component is unmounted
if (getMutateWithMiddlewares) {
mutateWithMiddlewares.current = getMutateWithMiddlewaresEvent((params) => {
return mutationFnEvent(params);
});
}
else {
mutateWithMiddlewares.current = mutationFnEvent;
}
// We need to keep the onSuccess callback here and not in the useMutation for undoable mutations
hasCallTimeOnSuccess.current = !!onSuccess;
// We need to store the onError and onSettled callbacks here to be able to call them in the useMutation hook
// so that they are called even when the calling component is unmounted
callTimeOnError.current = onError;
callTimeOnSettled.current = onSettled;
if (mutationMode) {
mode.current = mutationMode;
}
if (returnPromise && mode.current !== 'pessimistic') {
console.warn('The returnPromise parameter can only be used if the mutationMode is set to pessimistic');
}
snapshot.current = getSnapshotEvent(getQueryKeysEvent({ ...paramsRef.current, ...callTimeParams }, {
mutationMode: mode.current,
}));
if (mode.current === 'pessimistic') {
if (returnPromise) {
return mutation.mutateAsync({ ...paramsRef.current, ...callTimeParams },
// We don't pass onError and onSettled here as we will call them in the useMutation hook side effects
{ onSuccess, ...otherCallTimeOptions });
}
return mutation.mutate({ ...paramsRef.current, ...callTimeParams },
// We don't pass onError and onSettled here as we will call them in the useMutation hook side effects
{ onSuccess, ...otherCallTimeOptions });
}
// Cancel any outgoing re-fetches (so they don't overwrite our optimistic update)
await Promise.all(snapshot.current.map(([queryKey]) => queryClient.cancelQueries({ queryKey })));
// Optimistically update to the new value
const optimisticResult = updateCacheEvent({ ...paramsRef.current, ...callTimeParams }, {
mutationMode: mode.current,
}, undefined);
// run the success callbacks during the next tick
setTimeout(() => {
if (onSuccess) {
onSuccess(optimisticResult, { ...paramsRef.current, ...callTimeParams }, {
snapshot: snapshot.current,
}, {
client: queryClient,
mutationKey,
meta: mutationOptions.meta,
});
}
else if (mutationOptions.onSuccess &&
!hasCallTimeOnSuccess.current) {
mutationOptions.onSuccess(optimisticResult, { ...paramsRef.current, ...callTimeParams }, {
snapshot: snapshot.current,
}, {
client: queryClient,
mutationKey,
meta: mutationOptions.meta,
});
}
}, 0);
if (mode.current === 'optimistic') {
// call the mutate method without success side effects
return mutation.mutate({
...paramsRef.current,
...callTimeParams,
});
}
else {
// Undoable mutation: add the mutation to the undoable queue.
// The Notification component will dequeue it when the user confirms or cancels the message.
addUndoableMutation(({ isUndo }) => {
if (isUndo) {
if (onUndo) {
onUndoEvent({
...paramsRef.current,
...callTimeParams,
}, {
mutationMode: mode.current,
});
}
// rollback
snapshot.current.forEach(([key, value]) => {
queryClient.setQueryData(key, value);
});
}
else {
// call the mutate method without success side effects
mutation.mutate({
...paramsRef.current,
...callTimeParams,
});
}
});
}
};
const mutationResult = (0, react_1.useMemo)(() => ({
isLoading: mutation.isPending,
...mutation,
}), [mutation]);
return [(0, util_1.useEvent)(mutate), mutationResult];
};
exports.useMutationWithMutationMode = useMutationWithMutationMode;
const noop = () => { };
//# sourceMappingURL=useMutationWithMutationMode.js.map