Skip to content

Commit 32ca278

Browse files
author
Ruben van Leeuwen
committed
2076: Rearrange useEffects. Remove rhf from zodRule definitions
1 parent 20ce7fb commit 32ca278

File tree

5 files changed

+111
-208
lines changed

5 files changed

+111
-208
lines changed

frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx

Lines changed: 95 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import React, {
1414
useState,
1515
} from 'react';
1616
import { FieldValues, useForm } from 'react-hook-form';
17-
import { Subscription } from 'react-hook-form/dist/utils/createSubject';
1817

1918
import _ from 'lodash';
2019
import { z } from 'zod/v4';
@@ -54,7 +53,6 @@ function PydanticFormContextProvider({
5453
successNotice,
5554
onSuccess,
5655
onCancel,
57-
onChange,
5856
children,
5957
config,
6058
}: PydanticFormInitialContextProps) {
@@ -65,7 +63,6 @@ function PydanticFormContextProvider({
6563
customDataProviderCacheKey,
6664
formStructureMutator,
6765
fieldDetailProvider,
68-
onFieldChangeHandler,
6966
resetButtonAlternative,
7067
allowUntouchedSubmit,
7168
skipSuccessNotice,
@@ -81,21 +78,6 @@ function PydanticFormContextProvider({
8178

8279
const formRef = useRef<string>(formKey);
8380

84-
useEffect(() => {
85-
const getLocale = () => {
86-
switch (locale) {
87-
case Locale.enGB:
88-
return z.locales.en();
89-
case Locale.nlNL:
90-
return z.locales.nl();
91-
default:
92-
return z.locales.en();
93-
}
94-
};
95-
96-
z.config(getLocale());
97-
}, [locale]);
98-
9981
const updateHistory = async (
10082
formInput: object,
10183
previousSteps: object[],
@@ -131,10 +113,7 @@ function PydanticFormContextProvider({
131113
const [isFullFilled, setIsFullFilled] = useState(false);
132114
const [isSending, setIsSending] = useState(false);
133115

134-
// TODO: Fix leave confirmation functionality
135-
const [, setSaveToLeavePageInCurrentState] = useState(false);
136-
137-
// fetch the labels of the form, but can also include the current form values
116+
// fetch the labels of the form, can also contain default values
138117
const { data: formLabels, isLoading: isLoadingFormLabels } =
139118
useLabelProvider(labelProvider, formKey, formIdKey);
140119

@@ -144,20 +123,16 @@ function PydanticFormContextProvider({
144123
customDataProvider,
145124
);
146125

147-
// fetch the form definition using SWR hook
148126
const {
149127
data: apiResponse,
150128
isLoading: isLoadingSchema,
151129
error,
152130
} = useApiProvider(formKey, formInputData, apiProvider, metaData);
153131

154-
// we cache the form scheme so when there is an error, we still have the form
155-
// the form is not in the error response
156132
const [rawSchema, setRawSchema] = useState<PydanticFormSchemaRawJson>();
157133
const [hasNext, setHasNext] = useState<boolean>(false);
158134

159135
// extract the JSON schema to a more usable custom schema
160-
161136
const { pydanticFormSchema, isLoading: isParsingSchema } =
162137
usePydanticFormParser(
163138
rawSchema,
@@ -166,12 +141,9 @@ function PydanticFormContextProvider({
166141
formStructureMutator,
167142
);
168143

169-
const rhfRef = useRef<ReturnType<typeof useForm>>();
170-
171144
// build validation rules based on custom schema
172145
const zodSchema = useGetZodValidator(
173146
pydanticFormSchema,
174-
rhfRef.current,
175147
componentMatcherExtender,
176148
);
177149

@@ -197,42 +169,99 @@ function PydanticFormContextProvider({
197169
mode: 'all',
198170
defaultValues: initialData,
199171
values: initialData,
172+
shouldUnregister: true,
200173
});
201174

175+
const submitFormFn = useCallback(() => {
176+
setIsSending(true);
177+
const rhfValues = rhf.getValues();
178+
// Note. If we don't use cloneDeep here we are adding a reference to the rhfValues
179+
// that changes on every change in the form and triggering effects before we want to.
180+
addFormInputData(_.cloneDeep(rhfValues), !!errorDetails);
181+
window.scrollTo(0, 0);
182+
}, [rhf, errorDetails, addFormInputData]);
183+
184+
const onClientSideError = useCallback(
185+
(data?: FieldValues) => {
186+
// TODO implement save with errors toggle
187+
if (data) {
188+
rhf.clearErrors();
189+
submitFormFn();
190+
}
191+
},
192+
[rhf, submitFormFn],
193+
);
194+
195+
const submitForm = rhf.handleSubmit(submitFormFn, onClientSideError);
196+
197+
const resetForm = useCallback(
198+
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
199+
e.preventDefault();
200+
setErrorDetails(undefined);
201+
rhf.reset();
202+
rhf.trigger();
203+
},
204+
[rhf],
205+
);
206+
207+
const resetErrorDetails = useCallback(() => {
208+
setErrorDetails(undefined);
209+
}, []);
210+
211+
const isLoading =
212+
isLoadingFormLabels ||
213+
isLoadingSchema ||
214+
isParsingSchema ||
215+
(customDataProvider ? isLoadingCustomData : false);
216+
217+
const clearForm = useCallback(() => {
218+
setFormInputData([]);
219+
setIsFullFilled(false);
220+
setRawSchema(undefined);
221+
setHasNext(false);
222+
}, []);
223+
224+
const PydanticFormContextState = {
225+
// to prevent an issue where the sending state hangs
226+
// we check both the SWR hook state as our manual state
227+
isSending: isSending && isLoadingSchema,
228+
isLoading,
229+
rhf,
230+
pydanticFormSchema,
231+
loadingComponent,
232+
onPrevious: () => goToPreviousStep(rhf?.getValues()),
233+
onCancel,
234+
title,
235+
sendLabel,
236+
isFullFilled,
237+
customDataProvider,
238+
errorDetails,
239+
resetErrorDetails,
240+
successNotice,
241+
submitForm,
242+
resetForm,
243+
cancelButton,
244+
skipSuccessNotice,
245+
allowUntouchedSubmit,
246+
resetButtonAlternative,
247+
config,
248+
formKey,
249+
formIdKey,
250+
clearForm,
251+
formInputData,
252+
hasNext,
253+
initialData,
254+
};
255+
256+
/** UseEffects */
202257
useEffect(() => {
203258
if (formKey !== formRef.current) {
204259
// When the formKey changes we need to reset the form input data
205260
setFormInputData([]);
206261
setFormInputHistory(new Map<string, object>());
207-
rhf?.reset({});
208262
formRef.current = formKey;
209263
}
210-
}, [formKey, rhf]);
211-
212-
rhfRef.current = rhf;
213-
214-
// Adds watch subscripton on form values
215-
useEffect(() => {
216-
const sub = rhf.watch((values) => {
217-
setSaveToLeavePageInCurrentState(false);
218-
onChange?.(values);
219-
});
220-
221-
return () => sub.unsubscribe();
222-
}, [rhf, onChange]);
223-
224-
/* TODO: Reimplement
225-
// prevent user from navigating away when there are unsaved changes
226-
const hasUnsavedData =
227-
!saveToLeavePageInCurrentState &&
228-
!isFullFilled &&
229-
rhf.formState.isDirty;
230-
231-
useLeavePageConfirm(
232-
hasUnsavedData,
233-
'Er zijn aanpassingen in het formulier. \nWeet je zeker dat je de pagina wilt verlaten?',
234-
);
235-
*/
264+
}, [formKey]);
236265

237266
// handle successfull submits
238267
useEffect(() => {
@@ -252,7 +281,6 @@ function PydanticFormContextProvider({
252281
}
253282

254283
setFormInputHistory(new Map<string, object>());
255-
rhf.reset({});
256284
}, [apiResponse, isFullFilled, onSuccess, rhf, skipSuccessNotice]);
257285

258286
// a useeffect for whenever the error response updates
@@ -266,7 +294,6 @@ function PydanticFormContextProvider({
266294

267295
// when we receive a new form from JSON, we fully reset the form
268296
if (apiResponse?.form && rawSchema !== apiResponse.form) {
269-
rhf.reset({});
270297
setRawSchema(apiResponse.form);
271298
if (apiResponse.meta) {
272299
setHasNext(!!apiResponse.meta.hasNext);
@@ -282,7 +309,6 @@ function PydanticFormContextProvider({
282309
setIsSending(false);
283310
// eslint-disable-next-line react-hooks/exhaustive-deps
284311
}, [apiResponse]); // Avoid completing the dependencies array here to avoid unwanted resetFormData calls
285-
286312
// a useeffect for filling data whenever formdefinition or labels update
287313

288314
useEffect(() => {
@@ -309,112 +335,20 @@ function PydanticFormContextProvider({
309335
});
310336
}, [error]);
311337

312-
const submitFormFn = useCallback(() => {
313-
setIsSending(true);
314-
const rhfValues = rhf.getValues();
315-
// Note. If we don't use cloneDeep here we are adding a reference to the rhfValues
316-
// that changes on every change in the form and triggering effects before we want to.
317-
addFormInputData(_.cloneDeep(rhfValues), !!errorDetails);
318-
window.scrollTo(0, 0);
319-
}, [rhf, errorDetails, addFormInputData]);
320-
321-
const onClientSideError = useCallback(
322-
(data?: FieldValues) => {
323-
// TODO implement save with errors toggle
324-
if (data) {
325-
rhf.clearErrors();
326-
submitFormFn();
327-
}
328-
},
329-
[rhf, submitFormFn],
330-
);
331-
332-
const submitForm = rhf.handleSubmit(submitFormFn, onClientSideError);
333-
334-
const resetForm = useCallback(
335-
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
336-
e.preventDefault();
337-
setErrorDetails(undefined);
338-
rhf.reset();
339-
rhf.trigger();
340-
},
341-
[rhf],
342-
);
343-
344-
const resetErrorDetails = useCallback(() => {
345-
setErrorDetails(undefined);
346-
}, []);
347-
348-
// with this we have the possibility to have listeners for specific fields
349-
// this could be used to trigger validations of related fields, casting changes to elsewhere, etc.
350338
useEffect(() => {
351-
let sub: Subscription;
352-
353-
if (onFieldChangeHandler) {
354-
sub = rhf.watch((value, { name, type }) => {
355-
onFieldChangeHandler(
356-
{
357-
name,
358-
type,
359-
value,
360-
},
361-
rhf,
362-
);
363-
});
364-
}
365-
366-
return () => {
367-
if (sub) {
368-
return sub.unsubscribe();
339+
const getLocale = () => {
340+
switch (locale) {
341+
case Locale.enGB:
342+
return z.locales.en();
343+
case Locale.nlNL:
344+
return z.locales.nl();
345+
default:
346+
return z.locales.en();
369347
}
370348
};
371-
}, [rhf, onFieldChangeHandler]);
372349

373-
const isLoading =
374-
isLoadingFormLabels ||
375-
isLoadingSchema ||
376-
isParsingSchema ||
377-
(customDataProvider ? isLoadingCustomData : false);
378-
379-
const clearForm = useCallback(() => {
380-
setFormInputData([]);
381-
setIsFullFilled(false);
382-
setRawSchema(undefined);
383-
setHasNext(false);
384-
}, []);
385-
386-
const PydanticFormContextState = {
387-
// to prevent an issue where the sending state hangs
388-
// we check both the SWR hook state as our manual state
389-
isSending: isSending && isLoadingSchema,
390-
isLoading,
391-
rhf,
392-
pydanticFormSchema,
393-
loadingComponent,
394-
onPrevious: () => goToPreviousStep(rhf?.getValues()),
395-
onCancel,
396-
title,
397-
sendLabel,
398-
isFullFilled,
399-
customDataProvider,
400-
errorDetails,
401-
resetErrorDetails,
402-
successNotice,
403-
submitForm,
404-
resetForm,
405-
cancelButton,
406-
skipSuccessNotice,
407-
allowUntouchedSubmit,
408-
resetButtonAlternative,
409-
config,
410-
setSaveToLeavePageInCurrentState,
411-
formKey,
412-
formIdKey,
413-
clearForm,
414-
formInputData,
415-
hasNext,
416-
initialData,
417-
};
350+
z.config(getLocale());
351+
}, [locale]);
418352

419353
return (
420354
<PydanticFormContext.Provider value={PydanticFormContextState}>

frontend/packages/pydantic-forms/src/core/helper.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,6 @@ export const getMatcher = (
477477

478478
export const getClientSideValidationRule = (
479479
pydanticFormField: PydanticFormField | undefined,
480-
rhf?: ReturnType<typeof useForm>,
481480
componentMatcherExtender?: PydanticFormsContextConfig['componentMatcherExtender'],
482481
): ZodType => {
483482
if (!pydanticFormField) return z.unknown();
@@ -486,7 +485,7 @@ export const getClientSideValidationRule = (
486485
const componentMatch = matcher(pydanticFormField);
487486

488487
let validationRule =
489-
componentMatch?.validator?.(pydanticFormField, rhf) ?? z.unknown();
488+
componentMatch?.validator?.(pydanticFormField) ?? z.unknown();
490489

491490
if (!pydanticFormField.required) {
492491
validationRule = validationRule.optional();

0 commit comments

Comments
 (0)