Skip to content

Commit

Permalink
feat(conform-react): stop relying on the state snapshot (#467)
Browse files Browse the repository at this point in the history
  • Loading branch information
edmundhung authored Apr 1, 2024
1 parent b701f21 commit ddbbcc4
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 53 deletions.
2 changes: 1 addition & 1 deletion packages/conform-dom/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
86 changes: 54 additions & 32 deletions packages/conform-react/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,13 @@ export function getMetadata<
FormError,
FormSchema extends Record<string, any>,
>(
formId: FormId<FormSchema, FormError>,
state: FormState<FormError>,
context: FormContext<FormSchema, FormError, any>,
subjectRef: MutableRefObject<SubscriptionSubject>,
stateSnapshot: FormState<FormError>,
name: FieldName<Schema, FormSchema, FormError> = '',
): Metadata<Schema, FormSchema, FormError> {
const id = name ? `${formId}-${name}` : formId;
const id = name ? `${context.formId}-${name}` : context.formId;
const state = context.getState();

return new Proxy(
{
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -305,23 +317,25 @@ export function getFieldMetadata<
FormSchema extends Record<string, any>,
FormError,
>(
formId: FormId<FormSchema, FormError>,
state: FormState<FormError>,
context: FormContext<FormSchema, FormError, any>,
subjectRef: MutableRefObject<SubscriptionSubject>,
stateSnapshot: FormState<FormError>,
prefix = '',
key?: string | number,
): FieldMetadata<Schema, FormSchema, FormError> {
const name =
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':
Expand All @@ -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(
Expand All @@ -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,
),
);
};
}
Expand All @@ -362,16 +384,16 @@ export function getFormMetadata<
FormError = string[],
FormValue = Schema,
>(
formId: FormId<Schema, FormError>,
state: FormState<FormError>,
subjectRef: MutableRefObject<SubscriptionSubject>,
context: FormContext<Schema, FormError, FormValue>,
subjectRef: MutableRefObject<SubscriptionSubject>,
stateSnapshot: FormState<FormError>,
noValidate: boolean,
): FormMetadata<Schema, FormError> {
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 {
Expand Down
28 changes: 8 additions & 20 deletions packages/conform-react/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()];
}
Expand All @@ -110,16 +110,10 @@ export function useFormMetadata<
): FormMetadata<Schema, FormError> {
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<
Expand All @@ -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<FieldSchema, FormSchema, FormError>(
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];
}
6 changes: 6 additions & 0 deletions playground/app/routes/subscription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit ddbbcc4

Please sign in to comment.