chore: cleanup useEffect

This commit is contained in:
Ephraim Atta-Duncan
2025-08-21 21:58:14 +00:00
parent 231ef9c27e
commit db4d33d039
55 changed files with 637 additions and 643 deletions
@@ -1,23 +1,23 @@
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
export const useElementBounds = (elementOrSelector: HTMLElement | string, withScroll = false) => {
const [bounds, setBounds] = useState({
top: 0,
left: 0,
height: 0,
width: 0,
});
const [forceRecalc, setForceRecalc] = useState(0);
const calculateBounds = useCallback(() => {
const bounds = useMemo(() => {
const $el =
typeof elementOrSelector === 'string'
? document.querySelector<HTMLElement>(elementOrSelector)
: elementOrSelector;
if (!$el) {
throw new Error('Element not found');
return {
top: 0,
left: 0,
height: 0,
width: 0,
};
}
if (withScroll) {
@@ -32,15 +32,11 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
width,
height,
};
}, [elementOrSelector, withScroll]);
useEffect(() => {
setBounds(calculateBounds());
}, []);
}, [elementOrSelector, withScroll, forceRecalc]);
useEffect(() => {
const onResize = () => {
setBounds(calculateBounds());
setForceRecalc((prev) => prev + 1);
};
window.addEventListener('resize', onResize);
@@ -61,7 +57,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
}
const observer = new ResizeObserver(() => {
setBounds(calculateBounds());
setForceRecalc((prev) => prev + 1);
});
observer.observe($el);
@@ -69,7 +65,7 @@ export const useElementBounds = (elementOrSelector: HTMLElement | string, withSc
return () => {
observer.disconnect();
};
}, []);
}, [elementOrSelector]);
return bounds;
};
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { RefObject, useEffect, useState } from 'react';
import { useMemo } from 'react';
/**
* Calculate the width and height of a text element.
@@ -64,13 +64,7 @@ export function useElementScaleSize(
fontSize: number,
fontFamily: string,
) {
const [scalingFactor, setScalingFactor] = useState(1);
useEffect(() => {
const scaleSize = calculateTextScaleSize(container, text, `${fontSize}px`, fontFamily);
setScalingFactor(scaleSize);
}, [text, container, fontFamily, fontSize]);
return scalingFactor;
return useMemo(() => {
return calculateTextScaleSize(container, text, `${fontSize}px`, fontFamily);
}, [container, text, fontSize, fontFamily]);
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import type { Field } from '@prisma/client';
@@ -6,20 +6,20 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
export const useFieldPageCoords = (field: Field) => {
const [coords, setCoords] = useState({
x: 0,
y: 0,
height: 0,
width: 0,
});
const [forceRecalc, setForceRecalc] = useState(0);
const calculateCoords = useCallback(() => {
const coords = useMemo(() => {
const $page = document.querySelector<HTMLElement>(
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.page}"]`,
);
if (!$page) {
return;
return {
x: 0,
y: 0,
height: 0,
width: 0,
};
}
const { top, left, height, width } = getBoundingClientRect($page);
@@ -31,21 +31,17 @@ export const useFieldPageCoords = (field: Field) => {
const fieldHeight = (Number(field.height) / 100) * height;
const fieldWidth = (Number(field.width) / 100) * width;
setCoords({
return {
x: fieldX,
y: fieldY,
height: fieldHeight,
width: fieldWidth,
});
}, [field.height, field.page, field.positionX, field.positionY, field.width]);
useEffect(() => {
calculateCoords();
}, [calculateCoords]);
};
}, [field.height, field.page, field.positionX, field.positionY, field.width, forceRecalc]);
useEffect(() => {
const onResize = () => {
calculateCoords();
setForceRecalc((prev) => prev + 1);
};
window.addEventListener('resize', onResize);
@@ -53,7 +49,7 @@ export const useFieldPageCoords = (field: Field) => {
return () => {
window.removeEventListener('resize', onResize);
};
}, [calculateCoords]);
}, []);
useEffect(() => {
const $page = document.querySelector<HTMLElement>(
@@ -65,7 +61,7 @@ export const useFieldPageCoords = (field: Field) => {
}
const observer = new ResizeObserver(() => {
calculateCoords();
setForceRecalc((prev) => prev + 1);
});
observer.observe($page);
@@ -73,7 +69,7 @@ export const useFieldPageCoords = (field: Field) => {
return () => {
observer.disconnect();
};
}, [calculateCoords, field.page]);
}, [field.page]);
return coords;
};
@@ -1,11 +1,17 @@
import { useEffect, useState } from 'react';
import { useSyncExternalStore } from 'react';
const subscribe = () => {
return () => {};
};
const getSnapshot = () => {
return true;
};
const getServerSnapshot = () => {
return false;
};
export const useIsMounted = () => {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
return isMounted;
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
};
+21 -9
View File
@@ -1,11 +1,23 @@
import { useEffect, useState } from 'react';
import { useSyncExternalStore } from 'react';
export const ClientOnly = async ({ children }: { children: React.ReactNode }) => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted ? children : null;
const subscribe = () => {
return () => {};
};
const getSnapshot = () => {
return true;
};
const getServerSnapshot = () => {
return false;
};
export const ClientOnly = ({
children,
}: {
children: React.ReactNode;
}): React.ReactElement | null => {
const isClient = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isClient ? <>{children}</> : null;
};
@@ -18,7 +18,6 @@ import {
} from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useHotkeys } from 'react-hotkeys-hook';
import { prop, sortBy } from 'remeda';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
@@ -103,6 +102,10 @@ export const AddFieldsFormPartial = ({
const [isMissingSignatureDialogVisible, setIsMissingSignatureDialogVisible] = useState(false);
const handleMissingSignatureDialogOpenChange = (value: boolean) => {
setIsMissingSignatureDialogVisible(value);
};
const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement();
const { currentStep, totalSteps, previousStep } = useStep();
const canRenderBackButtonAsRemove =
@@ -511,18 +514,24 @@ export const AddFieldsFormPartial = ({
};
}, []);
useEffect(() => {
const defaultSelectedSigner = useMemo(() => {
const recipientsByRoleToDisplay = recipients.filter(
(recipient) =>
recipient.role !== RecipientRole.CC && recipient.role !== RecipientRole.ASSISTANT,
);
setSelectedSigner(
return (
recipientsByRoleToDisplay.find((r) => r.sendStatus !== SendStatus.SENT) ??
recipientsByRoleToDisplay[0],
recipientsByRoleToDisplay[0]
);
}, [recipients]);
if (selectedSigner && !recipients.find((r) => r.id === selectedSigner.id)) {
setSelectedSigner(defaultSelectedSigner);
} else if (!selectedSigner && defaultSelectedSigner) {
setSelectedSigner(defaultSelectedSigner);
}
const recipientsByRole = useMemo(() => {
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
CC: [],
@@ -539,29 +548,6 @@ export const AddFieldsFormPartial = ({
return recipientsByRole;
}, [recipients]);
const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][])
.filter(
([role]) =>
role !== RecipientRole.CC &&
role !== RecipientRole.VIEWER &&
role !== RecipientRole.ASSISTANT,
)
.map(
([role, roleRecipients]) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
[
role,
sortBy(
roleRecipients,
[(r) => r.signingOrder || Number.MAX_SAFE_INTEGER, 'asc'],
[prop('id'), 'asc'],
),
] as [RecipientRole, Recipient[]],
);
}, [recipientsByRole]);
const handleAdvancedSettings = () => {
setShowAdvancedSettings((prev) => !prev);
};
@@ -995,7 +981,7 @@ export const AddFieldsFormPartial = ({
<MissingSignatureFieldDialog
isOpen={isMissingSignatureDialogVisible}
onOpenChange={(value) => setIsMissingSignatureDialogVisible(value)}
onOpenChange={handleMissingSignatureDialogOpenChange}
/>
</>
)}
@@ -1,4 +1,4 @@
import { forwardRef, useEffect, useState } from 'react';
import { forwardRef, useMemo, useState } from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
@@ -160,22 +160,32 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
const defaultState: FieldMeta = getDefaultState(field.type);
const [fieldState, setFieldState] = useState(() => {
const savedState = localStorage.getItem(localStorageKey);
return savedState ? { ...defaultState, ...JSON.parse(savedState) } : defaultState;
});
useEffect(() => {
const initialFieldState = useMemo(() => {
if (fieldMeta && typeof fieldMeta === 'object') {
const parsedFieldMeta = ZFieldMetaSchema.parse(fieldMeta);
setFieldState({
return {
...defaultState,
...parsedFieldMeta,
});
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fieldMeta]);
const savedState = localStorage.getItem(localStorageKey);
return savedState ? { ...defaultState, ...JSON.parse(savedState) } : defaultState;
}, [fieldMeta, defaultState, localStorageKey]);
const [fieldState, setFieldState] = useState(initialFieldState);
if (fieldMeta && typeof fieldMeta === 'object') {
const parsedFieldMeta = ZFieldMetaSchema.parse(fieldMeta);
const expectedState = {
...defaultState,
...parsedFieldMeta,
};
if (JSON.stringify(fieldState) !== JSON.stringify(expectedState)) {
setFieldState(expectedState);
}
}
const handleFieldChange = (
key: FieldMetaKeys,
@@ -1,12 +1,12 @@
import type { MouseEvent, PointerEvent, RefObject, TouchEvent } from 'react';
import { useMemo, useRef, useState } from 'react';
import { useLayoutEffect } from 'react';
import { Trans } from '@lingui/react/macro';
import { Undo2 } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import {
SIGNATURE_CANVAS_DPI,
SIGNATURE_MIN_COVERAGE_THRESHOLD,
@@ -247,7 +247,7 @@ export const SignaturePadDraw = ({
onChange?.($el.current.toDataURL());
};
unsafe_useEffectOnce(() => {
useLayoutEffect(() => {
if ($el.current) {
$el.current.width = $el.current.clientWidth * SIGNATURE_CANVAS_DPI;
$el.current.height = $el.current.clientHeight * SIGNATURE_CANVAS_DPI;
@@ -270,7 +270,7 @@ export const SignaturePadDraw = ({
img.src = value;
}
});
}, [value]);
return (
<div className={cn('h-full w-full', className)}>
@@ -1,10 +1,9 @@
import { useRef } from 'react';
import { useLayoutEffect, useRef } from 'react';
import { Trans } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { UploadCloudIcon } from 'lucide-react';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { SIGNATURE_CANVAS_DPI } from '@documenso/lib/constants/signatures';
import { cn } from '../../lib/utils';
@@ -97,7 +96,7 @@ export const SignaturePadUpload = ({
}
};
unsafe_useEffectOnce(() => {
useLayoutEffect(() => {
// Todo: Not really sure if this is required for uploaded images.
if ($el.current) {
$el.current.width = $el.current.clientWidth * SIGNATURE_CANVAS_DPI;
@@ -121,7 +120,7 @@ export const SignaturePadUpload = ({
img.src = value;
}
});
}, [value]);
return (
<div className={cn('relative h-full w-full', className)}>
@@ -1,5 +1,3 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
@@ -16,7 +14,7 @@ import {
DOCUMENT_SIGNATURE_TYPES,
} from '@documenso/lib/constants/document';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
@@ -113,7 +111,8 @@ export const AddTemplateSettingsFormPartial = ({
meta: {
subject: template.templateMeta?.subject ?? '',
message: template.templateMeta?.message ?? '',
timezone: template.templateMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
timezone:
template.templateMeta?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
dateFormat: (template.templateMeta?.dateFormat ??
DEFAULT_DOCUMENT_DATE_FORMAT) as TDocumentMetaDateFormat,
@@ -152,14 +151,6 @@ export const AddTemplateSettingsFormPartial = ({
)
.otherwise(() => false);
// We almost always want to set the timezone to the user's local timezone to avoid confusion
// when the document is signed.
useEffect(() => {
if (!form.formState.touchedFields.meta?.timezone && !template.templateMeta?.timezone) {
form.setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone);
}
}, [form, form.setValue, form.formState.touchedFields.meta?.timezone]);
return (
<>
<DocumentFlowFormContainerHeader