diff --git a/packages/lib/client-only/providers/envelope-editor-provider.tsx b/packages/lib/client-only/providers/envelope-editor-provider.tsx index e61ed7ae0..ea49f638c 100644 --- a/packages/lib/client-only/providers/envelope-editor-provider.tsx +++ b/packages/lib/client-only/providers/envelope-editor-provider.tsx @@ -15,7 +15,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; import { useLingui } from '@lingui/react/macro'; import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client'; import type React from 'react'; -import { createContext, useCallback, useContext, useMemo, useRef, useState } from 'react'; +import { createContext, useCallback, useContext, useMemo, useRef, useState, useSyncExternalStore } from 'react'; import { useSearchParams } from 'react-router'; import type { TDocumentEmailSettings } from '../../types/document-email'; @@ -107,7 +107,39 @@ export const EnvelopeEditorProvider = ({ const [_searchParams, setSearchParams] = useSearchParams(); - const [envelope, _setEnvelope] = useState(initialEnvelope); + /** + * The envelope is kept in a ref-backed external store instead of useState so + * that async consumers (debounced autosave callbacks, flushAutosave, resetForms) + * can synchronously read the latest value via `getEnvelope`. + * + * React subscribes to the store through useSyncExternalStore, keeping renders in + * sync without maintaining a separate copy of the state. + */ + const envelopeStoreRef = useRef(initialEnvelope); + const envelopeStoreSubscribersRef = useRef(new Set<() => void>()); + + const subscribeToEnvelopeStore = useCallback((onStoreChange: () => void) => { + envelopeStoreSubscribersRef.current.add(onStoreChange); + + return () => { + envelopeStoreSubscribersRef.current.delete(onStoreChange); + }; + }, []); + + const getEnvelope = useCallback(() => envelopeStoreRef.current, []); + + const setEnvelope = useCallback((action: React.SetStateAction) => { + const next = typeof action === 'function' ? action(envelopeStoreRef.current) : action; + + envelopeStoreRef.current = next; + + for (const onStoreChange of envelopeStoreSubscribersRef.current) { + onStoreChange(); + } + }, []); + + const envelope = useSyncExternalStore(subscribeToEnvelopeStore, getEnvelope, getEnvelope); + const [autosaveError, setAutosaveError] = useState(false); const isCscMode = IS_INSTANCE_CSC_MODE(); @@ -135,8 +167,6 @@ export const EnvelopeEditorProvider = ({ }; }, [isCscMode, providedEditorConfig]); - const envelopeRef = useRef(initialEnvelope); - const externalFlushCallbacksRef = useRef Promise>>(new Map()); const pendingMutationsRef = useRef>>(new Set()); @@ -156,14 +186,6 @@ export const EnvelopeEditorProvider = ({ }); }, []); - const setEnvelope: typeof _setEnvelope = (action) => { - _setEnvelope((prev) => { - const next = typeof action === 'function' ? action(prev) : action; - envelopeRef.current = next; - return next; - }); - }; - const isEmbedded = editorConfig.embedded !== undefined; const editorFields = useEditorFields({ @@ -192,16 +214,18 @@ export const EnvelopeEditorProvider = ({ try { let recipients: TEditorEnvelope['recipients'] = []; + const currentEnvelope = getEnvelope(); + if (!isEmbedded) { const response = await setRecipientsMutation.mutateAsync({ - envelopeId: envelope.id, - envelopeType: envelope.type, + envelopeId: currentEnvelope.id, + envelopeType: currentEnvelope.type, recipients: localRecipients, }); recipients = response.data; } else { - recipients = mapLocalRecipientsToRecipients({ envelope, localRecipients }); + recipients = mapLocalRecipientsToRecipients({ envelope: currentEnvelope, localRecipients }); } setEnvelope((prev) => ({ @@ -211,9 +235,7 @@ export const EnvelopeEditorProvider = ({ })); // Reset the local fields to ensure deleted recipient fields are removed. - editorFields.resetForm( - envelope.fields.filter((field) => recipients.some((recipient) => recipient.id === field.recipientId)), - ); + editorFields.resetForm(getEnvelope().fields); setAutosaveError(false); } catch (err) { @@ -248,16 +270,18 @@ export const EnvelopeEditorProvider = ({ try { let fields: TSetEnvelopeFieldsResponse['data'] = []; + const currentEnvelope = getEnvelope(); + if (!isEmbedded) { const response = await setFieldsMutation.mutateAsync({ - envelopeId: envelope.id, - envelopeType: envelope.type, + envelopeId: currentEnvelope.id, + envelopeType: currentEnvelope.type, fields: localFields, }); fields = response.data; } else { - fields = mapLocalFieldsToFields({ envelope, localFields }); + fields = mapLocalFieldsToFields({ envelope: currentEnvelope, localFields }); } setEnvelope((prev) => ({ @@ -309,7 +333,7 @@ export const EnvelopeEditorProvider = ({ try { const response = !isEmbedded ? await updateEnvelopeMutation.mutateAsync({ - envelopeId: envelope.id, + envelopeId: getEnvelope().id, data, meta, }) @@ -467,12 +491,14 @@ export const EnvelopeEditorProvider = ({ }; const resetForms = () => { + const currentEnvelope = getEnvelope(); + editorRecipients.resetForm({ - recipients: envelopeRef.current.recipients, - documentMeta: envelopeRef.current.documentMeta, + recipients: currentEnvelope.recipients, + documentMeta: currentEnvelope.documentMeta, }); - editorFields.resetForm(envelopeRef.current.fields); + editorFields.resetForm(currentEnvelope.fields); }; const flushAutosave = async (): Promise => { @@ -488,7 +514,7 @@ export const EnvelopeEditorProvider = ({ await Promise.allSettled(Array.from(pendingMutationsRef.current)); } - return envelopeRef.current; + return getEnvelope(); }; return (