UNPKG

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
"use strict"; 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