Skip to content
Open
249 changes: 141 additions & 108 deletions packages/formik/src/Formik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,32 +195,31 @@ export function useFormik<Values extends FormikValues = FormikValues>({
}, []);

const runValidateHandler = React.useCallback(
(values: Values, field?: string): Promise<FormikErrors<Values>> => {
return new Promise((resolve, reject) => {
const maybePromisedErrors = (props.validate as any)(values, field);
if (maybePromisedErrors == null) {
// use loose null check here on purpose
resolve(emptyErrors);
} else if (isPromise(maybePromisedErrors)) {
(maybePromisedErrors as Promise<any>).then(
errors => {
resolve(errors || emptyErrors);
},
actualException => {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`Warning: An unhandled error was caught during validation in <Formik validate />`,
actualException
);
}

reject(actualException);
(
values: Values,
field?: string
): FormikErrors<Values> | Promise<FormikErrors<Values>> => {
const maybePromisedErrors = props.validate?.(values, field);
if (!maybePromisedErrors) {
// use loose null check here on purpose
return emptyErrors;
} else if (isPromise(maybePromisedErrors)) {
return maybePromisedErrors.then(
errors => errors || emptyErrors,
actualException => {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`Warning: An unhandled error was caught during validation in <Formik validate />`,
actualException
);
}
);
} else {
resolve(maybePromisedErrors);
}
});

return Promise.reject(actualException);
}
);
} else {
return maybePromisedErrors;
}
},
[props.validate]
);
Expand Down Expand Up @@ -269,29 +268,24 @@ export function useFormik<Values extends FormikValues = FormikValues>({
);

const runSingleFieldLevelValidation = React.useCallback(
(field: string, value: void | string): Promise<string> => {
return new Promise(resolve =>
resolve(fieldRegistry.current[field].validate(value) as string)
);
(field: string, value: void | string) => {
return fieldRegistry.current[field].validate(value);
},
[]
);

const runFieldLevelValidations = React.useCallback(
(values: Values): Promise<FormikErrors<Values>> => {
(values: Values): FormikErrors<Values> | Promise<FormikErrors<Values>> => {
const fieldKeysWithValidation: string[] = Object.keys(
fieldRegistry.current
).filter(f => isFunction(fieldRegistry.current[f].validate));

// Construct an array with all of the field validation functions
const fieldValidations: Promise<string>[] =
fieldKeysWithValidation.length > 0
? fieldKeysWithValidation.map(f =>
runSingleFieldLevelValidation(f, getIn(values, f))
)
: [Promise.resolve('DO_NOT_DELETE_YOU_WILL_BE_FIRED')]; // use special case ;)
const fieldValidations = fieldKeysWithValidation.map(f =>
runSingleFieldLevelValidation(f, getIn(values, f))
);

return Promise.all(fieldValidations).then((fieldErrorsList: string[]) =>
const processFieldErrors = (fieldErrorsList: (string | undefined)[]) =>
fieldErrorsList.reduce((prev, curr, index) => {
if (curr === 'DO_NOT_DELETE_YOU_WILL_BE_FIRED') {
return prev;
Expand All @@ -300,26 +294,43 @@ export function useFormik<Values extends FormikValues = FormikValues>({
prev = setIn(prev, fieldKeysWithValidation[index], curr);
}
return prev;
}, {})
);
}, {} as FormikErrors<Values>);
Comment thread
fbarbare marked this conversation as resolved.
Outdated

if (fieldValidations.some(isPromise)) {
return Promise.all(fieldValidations).then(processFieldErrors);
} else {
return processFieldErrors(fieldValidations as (string | undefined)[]);
}
},
[runSingleFieldLevelValidation]
);

// Run all validations and return the result
const runAllValidations = React.useCallback(
(values: Values) => {
return Promise.all([
const allValidations = [
runFieldLevelValidations(values),
props.validationSchema ? runValidationSchema(values) : {},
props.validate ? runValidateHandler(values) : {},
]).then(([fieldErrors, schemaErrors, validateErrors]) => {
];

const processAllValidations = ([
fieldErrors,
schemaErrors,
validateErrors,
]: FormikErrors<Values>[]) => {
const combinedErrors = deepmerge.all<FormikErrors<Values>>(
[fieldErrors, schemaErrors, validateErrors],
{ arrayMerge }
);
return combinedErrors;
});
};

if (allValidations.some(isPromise)) {
return Promise.all(allValidations).then(processAllValidations);
} else {
return processAllValidations(allValidations);
}
},
[
props.validate,
Expand All @@ -334,13 +345,21 @@ export function useFormik<Values extends FormikValues = FormikValues>({
const validateFormWithHighPriority = useEventCallback(
(values: Values = state.values) => {
dispatch({ type: 'SET_ISVALIDATING', payload: true });
return runAllValidations(values).then(combinedErrors => {

const processCombinedErrors = (combinedErrors: FormikErrors<Values>) => {
if (!!isMounted.current) {
dispatch({ type: 'SET_ISVALIDATING', payload: false });
dispatch({ type: 'SET_ERRORS', payload: combinedErrors });
}
return combinedErrors;
});
};

const maybePromisedErrors = runAllValidations(values);
if (isPromise(maybePromisedErrors)) {
return maybePromisedErrors.then(processCombinedErrors);
} else {
return processCombinedErrors(maybePromisedErrors);
}
}
);

Expand Down Expand Up @@ -418,7 +437,12 @@ export function useFormik<Values extends FormikValues = FormikValues>({
dispatchFn();
}
},
[props.initialErrors, props.initialStatus, props.initialTouched, props.onReset]
[
props.initialErrors,
props.initialStatus,
props.initialTouched,
props.onReset,
]
);

React.useEffect(() => {
Expand Down Expand Up @@ -739,67 +763,73 @@ export function useFormik<Values extends FormikValues = FormikValues>({

const submitForm = useEventCallback(() => {
dispatch({ type: 'SUBMIT_ATTEMPT' });
return validateFormWithHighPriority().then(
(combinedErrors: FormikErrors<Values>) => {
// In case an error was thrown and passed to the resolved Promise,
// `combinedErrors` can be an instance of an Error. We need to check
// that and abort the submit.
// If we don't do that, calling `Object.keys(new Error())` yields an
// empty array, which causes the validation to pass and the form
// to be submitted.

const isInstanceOfError = combinedErrors instanceof Error;
const isActuallyValid =
!isInstanceOfError && Object.keys(combinedErrors).length === 0;
if (isActuallyValid) {
// Proceed with submit...
//
// To respect sync submit fns, we can't simply wrap executeSubmit in a promise and
// _always_ dispatch SUBMIT_SUCCESS because isSubmitting would then always be false.
// This would be fine in simple cases, but make it impossible to disable submit
// buttons where people use callbacks or promises as side effects (which is basically
// all of v1 Formik code). Instead, recall that we are inside of a promise chain already,
// so we can try/catch executeSubmit(), if it returns undefined, then just bail.
// If there are errors, throw em. Otherwise, wrap executeSubmit in a promise and handle
// cleanup of isSubmitting on behalf of the consumer.
let promiseOrUndefined;
try {
promiseOrUndefined = executeSubmit();
// Bail if it's sync, consumer is responsible for cleaning up
// via setSubmitting(false)
if (promiseOrUndefined === undefined) {
return;
}
} catch (error) {
throw error;
}

return Promise.resolve(promiseOrUndefined)
.then(result => {
if (!!isMounted.current) {
dispatch({ type: 'SUBMIT_SUCCESS' });
}
return result;
})
.catch(_errors => {
if (!!isMounted.current) {
dispatch({ type: 'SUBMIT_FAILURE' });
// This is a legit error rejected by the onSubmit fn
// so we don't want to break the promise chain
throw _errors;
}
});
} else if (!!isMounted.current) {
// ^^^ Make sure Formik is still mounted before updating state
dispatch({ type: 'SUBMIT_FAILURE' });
// throw combinedErrors;
if (isInstanceOfError) {
throw combinedErrors;
const processErrors = (combinedErrors: FormikErrors<Values>) => {
// In case an error was thrown and passed to the resolved Promise,
// `combinedErrors` can be an instance of an Error. We need to check
// that and abort the submit.
// If we don't do that, calling `Object.keys(new Error())` yields an
// empty array, which causes the validation to pass and the form
// to be submitted.

const isInstanceOfError = combinedErrors instanceof Error;
const isActuallyValid =
!isInstanceOfError && Object.keys(combinedErrors).length === 0;
if (isActuallyValid) {
// Proceed with submit...
//
// To respect sync submit fns, we can't simply wrap executeSubmit in a promise and
// _always_ dispatch SUBMIT_SUCCESS because isSubmitting would then always be false.
// This would be fine in simple cases, but make it impossible to disable submit
// buttons where people use callbacks or promises as side effects (which is basically
// all of v1 Formik code). Instead, recall that we are inside of a promise chain already,
// so we can try/catch executeSubmit(), if it returns undefined, then just bail.
// If there are errors, throw em. Otherwise, wrap executeSubmit in a promise and handle
// cleanup of isSubmitting on behalf of the consumer.
let promiseOrUndefined;
try {
promiseOrUndefined = executeSubmit();
// Bail if it's sync, consumer is responsible for cleaning up
// via setSubmitting(false)
if (promiseOrUndefined === undefined) {
return;
}
} catch (error) {
throw error;
}

return Promise.resolve(promiseOrUndefined)
.then(result => {
if (!!isMounted.current) {
dispatch({ type: 'SUBMIT_SUCCESS' });
}
return result;
})
.catch(_errors => {
if (!!isMounted.current) {
dispatch({ type: 'SUBMIT_FAILURE' });
// This is a legit error rejected by the onSubmit fn
// so we don't want to break the promise chain
throw _errors;
}
});
} else if (!!isMounted.current) {
// ^^^ Make sure Formik is still mounted before updating state
dispatch({ type: 'SUBMIT_FAILURE' });
// throw combinedErrors;
if (isInstanceOfError) {
throw combinedErrors;
}
return;
}
);
return;
};

const maybePromisedErrors = validateFormWithHighPriority();
if (isPromise(maybePromisedErrors)) {
return maybePromisedErrors.then(processErrors);
} else {
return processErrors(maybePromisedErrors);
}
});

const handleSubmit = useEventCallback(
Expand Down Expand Up @@ -831,12 +861,15 @@ export function useFormik<Values extends FormikValues = FormikValues>({
}
}

submitForm().catch(reason => {
console.warn(
`Warning: An unhandled error was caught from submitForm()`,
reason
);
});
const maybePromise = submitForm();
if (isPromise(maybePromise)) {
maybePromise.catch(reason => {
console.warn(
`Warning: An unhandled error was caught from submitForm()`,
reason
);
});
}
}
);

Expand Down
Loading