-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
(BREAKING) Migrate from Formik to RHF #349
Conversation
package.json
Outdated
"react-markdown": "^7.1.2", | ||
"react-waypoint": "^10.3.0", | ||
"react-webcam": "^7.2.0", | ||
"yup": "^1.4.0" | ||
"yup": "^1.4.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove yup and formik plus other unused libraries
@ibacher @brandones and @denniskigen - Samuel would love your early impressions on this work to migrate the engine to RHF :) |
Wow, this is a big change, and deals with a lot of stuff I haven't really worked on. Sorry I'm not really in a good position to review this. |
@samuelmale resolve conflicts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throwing up a few comments for you.
const rootForm = useRef<FormContextProps>(); | ||
const subForms = useRef<Record<string, FormContextProps>>({}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we using Refs here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason I used refs is that they won’t trigger a re-render on subsequent state updates. Do you want me to use the standard useState
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I guess since we're not sending them anywhere it's probably ok.
setCurrentPage, | ||
handleConfirmQuestionDeletion, | ||
}}> | ||
{formProcessors.current && children} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we set formProcessors()
above, won't this condition always be true
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well it appears redundant for now because I'm currently hardcoding the form processors. I left myself a TODO which suggests that the processors will be promised.
// TODO: Manage and load processors from the registry
const formProcessors = useRef<Record<string, FormProcessorConstructor>>({
EncounterFormProcessor: EncounterFormProcessor,
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A slightly different way to handle it would be to put the form processors in a separate component with a separate context and just wrap the component in Suspense
until they're loaded, e.g.,
<Suspense fallback={null}>
<FormProcessorProvider>
{children}
</FormProcessorProvider>
</Suspense>
visit, | ||
}); | ||
const { formFields: rawFormFields, conceptReferences } = useFormFields(formJson); | ||
const { concepts: formFieldsConcepts, isLoading: isLoadingConcepts } = useConcepts(conceptReferences); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isLoading
here seems to be unused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also add that this was one of the sort of requests I'd hoped we could avoid with the o3forms endpoint, but if that doesn't work for some reason, let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that loading all the concepts is an expensive operation, and we should rely on backend optimization. I've tried loading a form using the O3 forms endpoint, but in my experience, it doesn't seem to resolve missing labels (by inferring them from the concept).
For us to fully depend on the O3 forms endpoint, we need to add support for resolving subforms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't seem to resolve missing labels
Ah, yes, it doesn't magically create label
elements. Basically, this was so, in the NGX engine, we could override the label with translations or, absent those, the concept name, since the concept name isn't always the appropriate string. That said, the endpoint should give you an element called "conceptReferences" which has, for every concept in the form, the concept's UUID and display name.
I guess that's a good point about subforms, but you could always just request them as separate forms for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the o3forms endpoints now supports subforms and correctly queries forms by name, I'm gonna create a ticket to track the work of fully leveraging this backend optimisation.
const { | ||
isLoadingInitialValues, | ||
initialValues, | ||
error: initialValuesError, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this value be used for something?
if (formFieldAdapters) { | ||
setProcessorContext((prev) => ({ | ||
...prev, | ||
...{ formFieldAdapters }, | ||
})); | ||
} | ||
if (formFieldValidators) { | ||
setProcessorContext((prev) => ({ | ||
...prev, | ||
...{ formFieldValidators }, | ||
})); | ||
} | ||
if (formFieldsWithMeta?.length) { | ||
setProcessorContext((prev) => ({ | ||
...prev, | ||
...{ formFields: formFieldsWithMeta }, | ||
})); | ||
} else if (rawFormFields?.length) { | ||
setProcessorContext((prev) => ({ | ||
...prev, | ||
...{ formFields: rawFormFields }, | ||
})); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something like this:
if (formFieldAdapters) { | |
setProcessorContext((prev) => ({ | |
...prev, | |
...{ formFieldAdapters }, | |
})); | |
} | |
if (formFieldValidators) { | |
setProcessorContext((prev) => ({ | |
...prev, | |
...{ formFieldValidators }, | |
})); | |
} | |
if (formFieldsWithMeta?.length) { | |
setProcessorContext((prev) => ({ | |
...prev, | |
...{ formFields: formFieldsWithMeta }, | |
})); | |
} else if (rawFormFields?.length) { | |
setProcessorContext((prev) => ({ | |
...prev, | |
...{ formFields: rawFormFields }, | |
})); | |
} | |
setProcessorContext((prev) => merge({}, prev, formFieldAdapters ?? {}, formFieldValidators ? {}, formFieldsWithMeta?.length ? {formFields: formFieldsWithMeta} : {}, rawFormFields?.length ? { formFields: rawFormFields } : {})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
merge
from lodash
to support deep merging, but otherwise Object.assign()
would be the same as this with many fewer setState()
calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good solution, but it ends up losing my object references in memory due to the deep clone. This is problematic because I would have to manually reconcile the fields in the formJson tree versus the flattened fields. This can lead to increased memory usage and potential inconsistencies, as I will end up with two separate instances for each form field.
I've ended doing something like:
setProcessorContext((prev) => ({
...prev,
...(formFieldAdapters && { formFieldAdapters }),
...(formFieldValidators && { formFieldValidators }),
...(formFieldsWithMeta?.length
? { formFields: formFieldsWithMeta }
: rawFormFields?.length
? { formFields: rawFormFields }
: {}),
}));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! Fair enough. Still... involves fewer calls to the setProcessContext()
, which is basically the whole point.
const subForms = useRef<Record<string, FormContextProps>>({}); | ||
const layoutType = useLayoutType(); | ||
const { isSubmitting, setIsSubmitting, onSubmit, onError, handleClose } = formSubmissionProps; | ||
const registerForm = (formId: string, context: FormContextProps, isSubForm: boolean) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be wrapped in useCallback()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also wonder about the isSubForm
parameter. Shouldn't the assumption be something like the first form registered is the root form and every other form is a subform, so something like:
if (!rootForm.current) {
rootForm.current = context;
} else {
subForm.current[formId] = context;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one!
getSubForms, | ||
getRootForm, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a use case for providing these methods to children? The only use I see of them is in the "submitting" hook and it's better if we expose as minimal an API as possible to each child.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice argument! Updating 🧑🍳
f1e7c8a
to
567aa8a
Compare
…to migration changes
eb4d069
to
d3e5222
Compare
Size Change: -417 kB (-26.75%) 🎉 Total Size: 1.14 MB
ℹ️ View Unchanged
|
Requirements
Summary
This PR introduces a major migration from Formik to React Hook Form, along with the implementation of a new form processing architecture. The changes focus on enhancing the form handling capabilities, improving performance, and providing a more flexible and maintainable codebase. The introduction of form processors abstracts the form engine from the underlying domain objects, allowing for more modular and reusable code. Additionally, the field submission handlers have been refactored to form-field adapters for better clarity and naming consistency. Form validation has also been updated to address specific challenges.
Major Changes Made
Migration from Formik to React Hook Form:
Introduction of Form Processors:
EncounterFormProcessor
. These processors handle tasks such as resolving dependencies, initializing form values, and processing submissions, ensuring a clean separation of concerns and enhancing code modularity.EncounterFormProcessor
Implementation:EncounterFormProcessor
to manage the encounter-specific logic, including preparing the form schema, processing submissions, and resolving dependencies.useCustomHooks
) to manage the loading state and context updates for encounters.Refactoring of Field Submission Handlers to Form-Field Adapters:
tearDown
function to the underlying interface of form-field adapters to handle any necessary cleanup.Form Validation Management:
Screenshots
TBD
Related Issue
https://openmrs.atlassian.net/browse/O3-2906
Other
Form Engine Migration Checklist
Pending tasks
Unmasked issues/bugs in the Migration course