UNPKG

ra-core

Version:

Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React

239 lines 11.4 kB
import { useEffect, useMemo, useRef } from 'react'; import { useMutation, useQueryClient, } from '@tanstack/react-query'; import { useAddUndoableMutation } from "./undo/useAddUndoableMutation.js"; import { useEvent } from "../util/index.js"; export const useMutationWithMutationMode = (params = {}, options) => { const queryClient = useQueryClient(); const addUndoableMutation = 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 = useEvent(mutationFn); const updateCacheEvent = useEvent(updateCache); const getQueryKeysEvent = useEvent(getQueryKeys); const getSnapshotEvent = 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 = useEvent(onUndo ?? noop); const getMutateWithMiddlewaresEvent = useEvent(getMutateWithMiddlewares ?? noop); const mode = useRef(mutationMode); 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 = useRef(params); // Ref that stores the snapshot of the state before the mutation to allow reverting it const snapshot = useRef([]); // Ref that stores the mutation with middlewares to avoid losing them if the calling component is unmounted const mutateWithMiddlewares = 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 = useRef(); const callTimeOnSettled = 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 = useRef(false); const mutation = 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 = useMemo(() => ({ isLoading: mutation.isPending, ...mutation, }), [mutation]); return [useEvent(mutate), mutationResult]; }; const noop = () => { }; //# sourceMappingURL=useMutationWithMutationMode.js.map