From f8f470aba0c269366b04eec4cf09bb0a630a2d23 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Wed, 27 Mar 2024 16:30:33 +0100 Subject: [PATCH 1/2] feat: metadata should always return latest value --- packages/conform-dom/form.ts | 2 +- packages/conform-react/context.tsx | 86 +++++++++++++++++++----------- packages/conform-react/hooks.ts | 28 +++------- 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/packages/conform-dom/form.ts b/packages/conform-dom/form.ts index 486c567c..dabe32f5 100644 --- a/packages/conform-dom/form.ts +++ b/packages/conform-dom/form.ts @@ -937,7 +937,7 @@ export function createFormContext< Object.assign(latestOptions, options); if (latestOptions.formId !== currentFormId) { - getFormElement()?.reset(); + updateFormMeta(createFormMeta(latestOptions, true)); } else if (options.lastResult && options.lastResult !== currentResult) { report(options.lastResult); } diff --git a/packages/conform-react/context.tsx b/packages/conform-react/context.tsx index 1322792c..aabf51f4 100644 --- a/packages/conform-react/context.tsx +++ b/packages/conform-react/context.tsx @@ -214,12 +214,13 @@ export function getMetadata< FormError, FormSchema extends Record, >( - formId: FormId, - state: FormState, + context: FormContext, subjectRef: MutableRefObject, + stateSnapshot: FormState, name: FieldName = '', ): Metadata { - const id = name ? `${formId}-${name}` : formId; + const id = name ? `${context.formId}-${name}` : context.formId; + const state = context.getState(); return new Proxy( { @@ -265,7 +266,13 @@ export function getMetadata< new Proxy({} as any, { get(target, key, receiver) { if (typeof key === 'string') { - return getFieldMetadata(formId, state, subjectRef, name, key); + return getFieldMetadata( + context, + subjectRef, + stateSnapshot, + name, + key, + ); } return Reflect.get(target, key, receiver); @@ -275,23 +282,28 @@ export function getMetadata< }, { get(target, key, receiver) { - switch (key) { - case 'key': - case 'initialValue': - case 'value': - case 'valid': - case 'dirty': - updateSubjectRef(subjectRef, name, key, 'name'); - break; - case 'errors': - case 'allErrors': - updateSubjectRef( - subjectRef, - name, - 'error', - key === 'errors' ? 'name' : 'prefix', - ); - break; + // We want to minize re-render by identifying whether the field is used in a callback only + // but there is no clear way to know if it is accessed during render or not + // if the stateSnapshot is not the latest, then it must be accessed in a callback + if (state === stateSnapshot) { + switch (key) { + case 'key': + case 'initialValue': + case 'value': + case 'valid': + case 'dirty': + updateSubjectRef(subjectRef, name, key, 'name'); + break; + case 'errors': + case 'allErrors': + updateSubjectRef( + subjectRef, + name, + 'error', + key === 'errors' ? 'name' : 'prefix', + ); + break; + } } return Reflect.get(target, key, receiver); @@ -305,9 +317,9 @@ export function getFieldMetadata< FormSchema extends Record, FormError, >( - formId: FormId, - state: FormState, + context: FormContext, subjectRef: MutableRefObject, + stateSnapshot: FormState, prefix = '', key?: string | number, ): FieldMetadata { @@ -315,13 +327,15 @@ export function getFieldMetadata< typeof key === 'undefined' ? prefix : formatPaths([...getPaths(prefix), key]); - const metadata = getMetadata(formId, state, subjectRef, name); return new Proxy({} as any, { get(_, key, receiver) { + const metadata = getMetadata(context, subjectRef, stateSnapshot, name); + const state = context.getState(); + switch (key) { case 'formId': - return formId; + return context.formId; case 'required': case 'minLength': case 'maxLength': @@ -335,7 +349,9 @@ export function getFieldMetadata< return () => { const initialValue = state.initialValue[name] ?? []; - updateSubjectRef(subjectRef, name, 'initialValue', 'name'); + if (state === stateSnapshot) { + updateSubjectRef(subjectRef, name, 'initialValue', 'name'); + } if (!Array.isArray(initialValue)) { throw new Error( @@ -346,7 +362,13 @@ export function getFieldMetadata< return Array(initialValue.length) .fill(0) .map((_, index) => - getFieldMetadata(formId, state, subjectRef, name, index), + getFieldMetadata( + context, + subjectRef, + stateSnapshot, + name, + index, + ), ); }; } @@ -362,16 +384,16 @@ export function getFormMetadata< FormError = string[], FormValue = Schema, >( - formId: FormId, - state: FormState, - subjectRef: MutableRefObject, context: FormContext, + subjectRef: MutableRefObject, + stateSnapshot: FormState, noValidate: boolean, ): FormMetadata { - const metadata = getMetadata(formId, state, subjectRef); - return new Proxy({} as any, { get(_, key, receiver) { + const metadata = getMetadata(context, subjectRef, stateSnapshot); + const state = context.getState(); + switch (key) { case 'context': return { diff --git a/packages/conform-react/hooks.ts b/packages/conform-react/hooks.ts index 594dfa53..64bf724c 100644 --- a/packages/conform-react/hooks.ts +++ b/packages/conform-react/hooks.ts @@ -92,9 +92,9 @@ export function useForm< }); const subjectRef = useSubjectRef(); - const state = useFormState(context, subjectRef); + const stateSnapshot = useFormState(context, subjectRef); const noValidate = useNoValidate(options.defaultNoValidate); - const form = getFormMetadata(formId, state, subjectRef, context, noValidate); + const form = getFormMetadata(context, subjectRef, stateSnapshot, noValidate); return [form, form.getFieldset()]; } @@ -110,16 +110,10 @@ export function useFormMetadata< ): FormMetadata { const subjectRef = useSubjectRef(); const context = useFormContext(formId); - const state = useFormState(context, subjectRef); + const stateSnapshot = useFormState(context, subjectRef); const noValidate = useNoValidate(options.defaultNoValidate); - return getFormMetadata( - context.formId, - state, - subjectRef, - context, - noValidate, - ); + return getFormMetadata(context, subjectRef, stateSnapshot, noValidate); } export function useField< @@ -137,20 +131,14 @@ export function useField< ] { const subjectRef = useSubjectRef(); const context = useFormContext(options.formId); - const state = useFormState(context, subjectRef); + const stateSnapshot = useFormState(context, subjectRef); const field = getFieldMetadata( - context.formId, - state, + context, subjectRef, + stateSnapshot, name, ); - const form = getFormMetadata( - context.formId, - state, - subjectRef, - context, - false, - ); + const form = getFormMetadata(context, subjectRef, stateSnapshot, false); return [field, form]; } From 620e57429e7b9780b19f42d00b1ce09fce946c08 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Thu, 28 Mar 2024 19:05:39 +0100 Subject: [PATCH 2/2] add basic test --- playground/app/routes/subscription.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/playground/app/routes/subscription.tsx b/playground/app/routes/subscription.tsx index 61ddde22..c1a84422 100644 --- a/playground/app/routes/subscription.tsx +++ b/playground/app/routes/subscription.tsx @@ -61,6 +61,12 @@ export default function Example() { onValidate: !noClientValidate ? ({ formData }) => parseWithZod(formData, { schema }) : undefined, + onSubmit(event) { + // We should be able to access the latest form / field metadata anytime + if (!form.dirty || !(fields.name.dirty || fields.message.dirty)) { + event.preventDefault(); + } + }, }); const name = fields.name.name; const message = fields.message.name;