Skip to content

Commit

Permalink
Bump React Hook Form to latest (#1933)
Browse files Browse the repository at this point in the history
* notes on async form submit and nav block

* version that seems to work

* upgrade RHF

* cleanup

* delete sleep in the mock handler

* bump RHF again

* lil tweak
  • Loading branch information
david-crespo authored Feb 8, 2024
1 parent 6c8f7a9 commit 3dd635a
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 29 deletions.
48 changes: 29 additions & 19 deletions app/components/form/FullPageForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ interface FullPageFormProps<TFieldValues extends FieldValues> {
error?: Error
form: UseFormReturn<TFieldValues>
loading?: boolean
onSubmit: (values: TFieldValues) => void
/**
* Use await mutateAsync(), otherwise you'll break the logic below that relies
* on knowing when the submit is done.
*/
onSubmit: (values: TFieldValues) => Promise<void>
/** Error from the API call */
submitError: ApiError | null
/**
Expand Down Expand Up @@ -53,22 +57,25 @@ export function FullPageForm<TFieldValues extends FieldValues>({
onSubmit,
submitError,
}: FullPageFormProps<TFieldValues>) {
const { isSubmitting, isDirty } = form.formState
const { isSubmitting, isDirty, isSubmitSuccessful } = form.formState

/*
Confirms with the user if they want to navigate away
if the form is dirty. Does not intercept everything e.g.
refreshes or closing the tab but serves to reduce
the possibility of a user accidentally losing their progress
*/
const blocker = useBlocker(isDirty)
// Confirms with the user if they want to navigate away if the form is
// dirty. Does not intercept everything e.g. refreshes or closing the tab
// but serves to reduce the possibility of a user accidentally losing their
// progress.
const blocker = useBlocker(isDirty && !isSubmitSuccessful)

// Reset blocker if form is no longer dirty
// Gating on !isSubmitSuccessful above makes the blocker stop blocking nav
// after a successful submit. However, this can take a little time (there is a
// render in between when isSubmitSuccessful is true but the blocker is still
// ready to block), so we also have this useEffect that lets blocked requests
// through if submit is succesful but the blocker hasn't gotten a chance to
// stop blocking yet.
useEffect(() => {
if (blocker.state === 'blocked' && !isDirty) {
blocker.reset()
if (blocker.state === 'blocked' && isSubmitSuccessful) {
blocker.proceed()
}
}, [blocker, isDirty])
}, [blocker, isSubmitSuccessful])

const childArray = flattenChildren(children)
const actions = pluckFirstOfType(childArray, Form.Actions)
Expand All @@ -81,24 +88,27 @@ export function FullPageForm<TFieldValues extends FieldValues>({
<form
className="ox-form pb-20"
id={id}
onSubmit={(e) => {
onSubmit={async (e) => {
// This modal being in a portal doesn't prevent the submit event
// from bubbling up out of the portal. Normally that's not a
// problem, but sometimes (e.g., instance create) we render the
// SideModalForm from inside another form, in which case submitting
// the inner form submits the outer form unless we stop propagation
e.stopPropagation()
// This resets `isDirty` whilst keeping the values meaning
// we are not prevented from navigating away by the blocker
form.reset({} as TFieldValues, { keepValues: true })
form.handleSubmit(onSubmit)(e)
// Important to await here so isSubmitSuccessful doesn't become true
// until the submit is actually successful. Note you must use await
// mutateAsync() inside onSubmit in order to make this wait
await form.handleSubmit(onSubmit)(e)
}}
autoComplete="off"
>
{childArray}
</form>

{blocker ? <ConfirmNavigation blocker={blocker} /> : null}
{/* rendering of the modal must be gated on isSubmitSuccessful because
there is a brief moment where isSubmitSuccessful is true but the proceed()
hasn't fired yet, which means we get a brief flash of this modal */}
{!isSubmitSuccessful && <ConfirmNavigation blocker={blocker} />}

{actions && (
<PageActions>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function CreateInstanceForm() {
? await readBlobAsBase64(values.userData)
: undefined

createInstance.mutate({
await createInstance.mutateAsync({
query: projectSelector,
body: {
name: values.name,
Expand Down
2 changes: 1 addition & 1 deletion libs/api-mocks/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export const handlers = makeHandlers({
const instances = db.instances.filter((i) => i.project_id === project.id)
return paginated(query, instances)
},
instanceCreate({ body, query }) {
async instanceCreate({ body, query }) {
const project = lookup.project(query)

if (body.name === 'no-default-pool') {
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"react-aria": "^3.31.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.12",
"react-hook-form": "^7.47.0",
"react-hook-form": "^7.50.1",
"react-is": "^18.2.0",
"react-merge-refs": "^2.1.1",
"react-router-dom": "^6.21.1",
Expand Down

0 comments on commit 3dd635a

Please sign in to comment.