fix: back envelope editor state with synchronous external store

This commit is contained in:
Lucas Smith
2026-07-28 17:18:18 +10:00
parent 4db4821288
commit 82e0b4a845
@@ -15,7 +15,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client'; import { EnvelopeType, Prisma, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
import type React from 'react'; 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 { useSearchParams } from 'react-router';
import type { TDocumentEmailSettings } from '../../types/document-email'; import type { TDocumentEmailSettings } from '../../types/document-email';
@@ -107,7 +107,39 @@ export const EnvelopeEditorProvider = ({
const [_searchParams, setSearchParams] = useSearchParams(); 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<TEditorEnvelope>) => {
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<boolean>(false); const [autosaveError, setAutosaveError] = useState<boolean>(false);
const isCscMode = IS_INSTANCE_CSC_MODE(); const isCscMode = IS_INSTANCE_CSC_MODE();
@@ -135,8 +167,6 @@ export const EnvelopeEditorProvider = ({
}; };
}, [isCscMode, providedEditorConfig]); }, [isCscMode, providedEditorConfig]);
const envelopeRef = useRef(initialEnvelope);
const externalFlushCallbacksRef = useRef<Map<string, () => Promise<void>>>(new Map()); const externalFlushCallbacksRef = useRef<Map<string, () => Promise<void>>>(new Map());
const pendingMutationsRef = useRef<Set<Promise<unknown>>>(new Set()); const pendingMutationsRef = useRef<Set<Promise<unknown>>>(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 isEmbedded = editorConfig.embedded !== undefined;
const editorFields = useEditorFields({ const editorFields = useEditorFields({
@@ -192,16 +214,18 @@ export const EnvelopeEditorProvider = ({
try { try {
let recipients: TEditorEnvelope['recipients'] = []; let recipients: TEditorEnvelope['recipients'] = [];
const currentEnvelope = getEnvelope();
if (!isEmbedded) { if (!isEmbedded) {
const response = await setRecipientsMutation.mutateAsync({ const response = await setRecipientsMutation.mutateAsync({
envelopeId: envelope.id, envelopeId: currentEnvelope.id,
envelopeType: envelope.type, envelopeType: currentEnvelope.type,
recipients: localRecipients, recipients: localRecipients,
}); });
recipients = response.data; recipients = response.data;
} else { } else {
recipients = mapLocalRecipientsToRecipients({ envelope, localRecipients }); recipients = mapLocalRecipientsToRecipients({ envelope: currentEnvelope, localRecipients });
} }
setEnvelope((prev) => ({ setEnvelope((prev) => ({
@@ -211,9 +235,7 @@ export const EnvelopeEditorProvider = ({
})); }));
// Reset the local fields to ensure deleted recipient fields are removed. // Reset the local fields to ensure deleted recipient fields are removed.
editorFields.resetForm( editorFields.resetForm(getEnvelope().fields);
envelope.fields.filter((field) => recipients.some((recipient) => recipient.id === field.recipientId)),
);
setAutosaveError(false); setAutosaveError(false);
} catch (err) { } catch (err) {
@@ -248,16 +270,18 @@ export const EnvelopeEditorProvider = ({
try { try {
let fields: TSetEnvelopeFieldsResponse['data'] = []; let fields: TSetEnvelopeFieldsResponse['data'] = [];
const currentEnvelope = getEnvelope();
if (!isEmbedded) { if (!isEmbedded) {
const response = await setFieldsMutation.mutateAsync({ const response = await setFieldsMutation.mutateAsync({
envelopeId: envelope.id, envelopeId: currentEnvelope.id,
envelopeType: envelope.type, envelopeType: currentEnvelope.type,
fields: localFields, fields: localFields,
}); });
fields = response.data; fields = response.data;
} else { } else {
fields = mapLocalFieldsToFields({ envelope, localFields }); fields = mapLocalFieldsToFields({ envelope: currentEnvelope, localFields });
} }
setEnvelope((prev) => ({ setEnvelope((prev) => ({
@@ -309,7 +333,7 @@ export const EnvelopeEditorProvider = ({
try { try {
const response = !isEmbedded const response = !isEmbedded
? await updateEnvelopeMutation.mutateAsync({ ? await updateEnvelopeMutation.mutateAsync({
envelopeId: envelope.id, envelopeId: getEnvelope().id,
data, data,
meta, meta,
}) })
@@ -467,12 +491,14 @@ export const EnvelopeEditorProvider = ({
}; };
const resetForms = () => { const resetForms = () => {
const currentEnvelope = getEnvelope();
editorRecipients.resetForm({ editorRecipients.resetForm({
recipients: envelopeRef.current.recipients, recipients: currentEnvelope.recipients,
documentMeta: envelopeRef.current.documentMeta, documentMeta: currentEnvelope.documentMeta,
}); });
editorFields.resetForm(envelopeRef.current.fields); editorFields.resetForm(currentEnvelope.fields);
}; };
const flushAutosave = async (): Promise<TEditorEnvelope> => { const flushAutosave = async (): Promise<TEditorEnvelope> => {
@@ -488,7 +514,7 @@ export const EnvelopeEditorProvider = ({
await Promise.allSettled(Array.from(pendingMutationsRef.current)); await Promise.allSettled(Array.from(pendingMutationsRef.current));
} }
return envelopeRef.current; return getEnvelope();
}; };
return ( return (