Skip to content

Commit

Permalink
Merge branch 'master' into changeset-release/master
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] authored Nov 11, 2020
2 parents 7df82a0 + 1a9688c commit 4d1fc93
Show file tree
Hide file tree
Showing 5 changed files with 12 additions and 266 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-cameras-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'formik': patch
---

Remove low-priority validation implementation
2 changes: 0 additions & 2 deletions packages/formik/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"lodash": "^4.17.14",
"lodash-es": "^4.17.14",
"react-fast-compare": "^2.0.1",
"scheduler": "^0.20.1",
"tiny-warning": "^1.0.2",
"tslib": "^1.10.0"
},
Expand All @@ -48,7 +47,6 @@
"@types/lodash": "^4.14.119",
"@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9",
"@types/scheduler": "^0.16.1",
"@types/warning": "^3.0.0",
"@types/yup": "^0.24.9",
"just-debounce-it": "^1.1.0",
Expand Down
90 changes: 7 additions & 83 deletions packages/formik/src/Formik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ import {
} from './utils';
import { FormikProvider } from './FormikContext';
import invariant from 'tiny-warning';
import {
unstable_LowPriority,
unstable_runWithPriority,
unstable_scheduleCallback,
} from 'scheduler';

type FormikMessage<Values> =
| { type: 'SUBMIT_ATTEMPT' }
Expand All @@ -46,10 +41,6 @@ type FormikMessage<Values> =
| { type: 'SET_FIELD_ERROR'; payload: { field: string; value?: string } }
| { type: 'SET_TOUCHED'; payload: FormikTouched<Values> }
| { type: 'SET_ERRORS'; payload: FormikErrors<Values> }
| {
type: 'SET_LOW_PRIORITY_ERRORS';
payload: { values: Values; errors: FormikErrors<Values> };
}
| { type: 'SET_STATUS'; payload: any }
| {
type: 'SET_FORMIK_STATE';
Expand All @@ -76,23 +67,6 @@ function formikReducer<Values>(
}

return { ...state, errors: msg.payload };
case 'SET_LOW_PRIORITY_ERRORS':
if (
// Low priority validation can occur after high priority validation and
// this will create stale validation results, e.g:
// SET_FIELD_VALUE-------------------SET_LOW_PRIORITY_ERRORS
// SET_FIELD_VALUE-------------------SET_LOW_PRIORITY_ERRORS
// SET_ISVALIDATING-SET_ERRORS-SET_ISVALIDATING
//
// So we want to skip validation results if values are not the same
// anymore.
isEqual(state.errors, msg.payload.errors) ||
!isEqual(state.values, msg.payload.values)
) {
return state;
}

return { ...state, errors: msg.payload.errors };
case 'SET_STATUS':
return { ...state, status: msg.payload };
case 'SET_ISSUBMITTING':
Expand Down Expand Up @@ -344,46 +318,6 @@ export function useFormik<Values extends FormikValues = FormikValues>({
]
);

// Run validations and dispatching the result as low-priority via rAF.
//
// The thinking is that validation as a result of onChange and onBlur
// should never block user input. Note: This method should never be called
// during the submission phase because validation prior to submission
// is actually high-priority since we absolutely need to guarantee the
// form is valid before executing props.onSubmit.
const validateFormWithLowPriority = useEventCallback(
(
values: Values = state.values
): Promise<FormikErrors<Values>> | Promise<void> => {
return (runWithLowPriority(() => {
return runAllValidations(values)
.then(combinedErrors => {
if (!!isMounted.current) {
dispatch({
type: 'SET_LOW_PRIORITY_ERRORS',
payload: { values, errors: combinedErrors },
});
}
return combinedErrors;
})
.catch(actualException => {
if (process.env.NODE_ENV !== 'production') {
// Users can throw during validate, however they have no way of handling their error on touch / blur. In low priority, we need to handle it
console.warn(
`Warning: An unhandled error was caught during low priority validation in <Formik validate />`,
actualException
);
}
});
// The scheduler package is a transitive dependency installed with React
// If we leave this type as is, scheduler types leak and be exported in the build
// which would require folks to install @types/scheduler if they had
// skipLibCheck: false. Or we'd have to add @types/scheduler as a dep.
// Both are unecessary.
}) as unknown) as Promise<FormikErrors<Values>> | Promise<void>;
}
);

// Run all validations methods and update state accordingly
const validateFormWithHighPriority = useEventCallback(
(values: Values = state.values) => {
Expand All @@ -406,9 +340,9 @@ export function useFormik<Values extends FormikValues = FormikValues>({
isMounted.current === true &&
isEqual(initialValues.current, props.initialValues)
) {
validateFormWithLowPriority(initialValues.current);
validateFormWithHighPriority(initialValues.current);
}
}, [validateOnMount, validateFormWithLowPriority]);
}, [validateOnMount, validateFormWithHighPriority]);

const resetForm = React.useCallback(
(nextState?: Partial<FormikState<Values>>) => {
Expand Down Expand Up @@ -488,15 +422,15 @@ export function useFormik<Values extends FormikValues = FormikValues>({
}

if (validateOnMount) {
validateFormWithLowPriority(initialValues.current);
validateFormWithHighPriority(initialValues.current);
}
}
}, [
enableReinitialize,
props.initialValues,
resetForm,
validateOnMount,
validateFormWithLowPriority,
validateFormWithHighPriority,
]);

React.useEffect(() => {
Expand Down Expand Up @@ -606,7 +540,7 @@ export function useFormik<Values extends FormikValues = FormikValues>({
const willValidate =
shouldValidate === undefined ? validateOnBlur : shouldValidate;
return willValidate
? validateFormWithLowPriority(state.values)
? validateFormWithHighPriority(state.values)
: Promise.resolve();
}
);
Expand All @@ -623,7 +557,7 @@ export function useFormik<Values extends FormikValues = FormikValues>({
const willValidate =
shouldValidate === undefined ? validateOnChange : shouldValidate;
return willValidate
? validateFormWithLowPriority(resolvedValues)
? validateFormWithHighPriority(resolvedValues)
: Promise.resolve();
}
);
Expand All @@ -650,7 +584,7 @@ export function useFormik<Values extends FormikValues = FormikValues>({
const willValidate =
shouldValidate === undefined ? validateOnChange : shouldValidate;
return willValidate
? validateFormWithLowPriority(setIn(state.values, field, value))
? validateFormWithHighPriority(setIn(state.values, field, value))
: Promise.resolve();
}
);
Expand Down Expand Up @@ -899,7 +833,6 @@ export function useFormik<Values extends FormikValues = FormikValues>({

const imperativeMethods: FormikHelpers<Values> = {
resetForm,

validateForm: validateFormWithHighPriority,
validateField,
setErrors,
Expand Down Expand Up @@ -1195,15 +1128,6 @@ function arrayMerge(target: any[], source: any[], options: any): any[] {
return destination;
}

/**
* Schedule function as low priority by the scheduler API
*/
function runWithLowPriority(fn: () => any) {
return unstable_runWithPriority(unstable_LowPriority, () =>
unstable_scheduleCallback(unstable_LowPriority, fn)
);
}

/** Return multi select values based on an array of options */
function getSelectedValues(options: any[]) {
return Array.from(options)
Expand Down
176 changes: 0 additions & 176 deletions packages/formik/test/Formik.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1450,180 +1450,4 @@ describe('<Formik>', () => {

expect(innerRef.current).toEqual(getProps());
});

describe('low priority validation', () => {
function renderForm(props?: Partial<FormikProps<{ name: string }>>) {
const validate = jest.fn(({ name }) =>
name == 'ian' ? {} : { name: 'no' }
);
const renderedErrors: Array<[string, string]> = [];

function createForm(props?: Partial<FormikProps<{ name: string }>>) {
return (
<Formik
onSubmit={noop}
validate={validate}
initialValues={{ name: '' }}
{...props}
>
{({ values, errors, handleBlur, handleChange, handleSubmit }) => {
if (errors.name) {
renderedErrors.push([values.name, errors.name]);
}

return (
<form name="form" onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={values.name}
onBlur={handleBlur}
onChange={handleChange}
/>
</form>
);
}}
</Formik>
);
}

const api = render(createForm(props));

function rerender(overrides?: Partial<FormikProps<{ name: string }>>) {
api.rerender(createForm({ ...props, ...overrides }));
}

return { ...api, rerender, validate, renderedErrors };
}

it('bails low priority validations on mount', async () => {
const { getByRole, validate, renderedErrors } = renderForm({
validateOnMount: true,
});

expect(validate).not.toBeCalled();

act(() => {
fireEvent.change(getByRole('textbox'), {
persist: noop,
target: { name: 'name', value: 'ian' },
});
});

expect(validate).not.toBeCalled();
expect(renderedErrors).toHaveLength(0);

act(() => {
fireEvent.submit(getByRole('form'));
});

expect(renderedErrors).toHaveLength(0);
expect(validate).toBeCalledTimes(1);
expect(validate).lastCalledWith({ name: 'ian' }, undefined);

await waitFor(() => {
expect(validate).toBeCalledTimes(3);
expect(validate.mock.calls).toEqual([
// Triggered by submit
[{ name: 'ian' }, undefined],
// Scheduled on first mount
[{ name: '' }, undefined],
// Scheduled on by change
[{ name: 'ian' }, undefined],
]);
});

expect(renderedErrors).toHaveLength(0);
});

it('bails low priority validations on reinitialize', async () => {
const { getByRole, validate, renderedErrors, rerender } = renderForm({
validateOnMount: true,
enableReinitialize: true,
});

expect(validate).not.toBeCalled();

rerender({ initialValues: { name: 'ian' } });

expect(validate).not.toBeCalled();
expect(renderedErrors).toHaveLength(0);

act(() => {
fireEvent.submit(getByRole('form'));
});

expect(validate).toBeCalledTimes(1);
expect(renderedErrors).toHaveLength(0);

await waitFor(() => {
expect(validate).toBeCalledTimes(3);
expect(validate.mock.calls).toEqual([
// Triggered by submit
[{ name: 'ian' }, undefined],
// Scheduled on first mount
[{ name: '' }, undefined],
// Scheduled on second mount
[{ name: 'ian' }, undefined],
]);
});

expect(renderedErrors).toHaveLength(0);
});

it('bails low priority validations on change', async () => {
const { validate, getByRole, renderedErrors } = renderForm({
initialValues: { name: '' },
});

expect(validate).not.toBeCalled();

act(() => {
fireEvent.change(getByRole('textbox'), {
persist: noop,
target: { name: 'name', value: 'i' },
});
});

act(() => {
fireEvent.change(getByRole('textbox'), {
persist: noop,
target: { name: 'name', value: 'ia' },
});
});

act(() => {
fireEvent.change(getByRole('textbox'), {
persist: noop,
target: { name: 'name', value: 'ian' },
});
});

expect(validate).not.toBeCalled();
expect(renderedErrors).toHaveLength(0);

act(() => {
fireEvent.submit(getByRole('form'));
});

expect(validate).toBeCalledTimes(1);
expect(renderedErrors).toHaveLength(0);

await waitFor(() => {
expect(validate).toBeCalledTimes(4);
expect(validate.mock.calls).toEqual([
// Triggered by submit
[{ name: 'ian' }, undefined],
// Scheduled on first change
[{ name: 'i' }, undefined],
// Scheduled on second change
[{ name: 'ia' }, undefined],
// Scheduled on third change
[{ name: 'ian' }, undefined],
]);
});

expect(renderedErrors).toHaveLength(0);
});
});
});
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3108,11 +3108,6 @@
dependencies:
"@types/node" "*"

"@types/scheduler@^0.16.1":
version "0.16.1"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==

"@types/semver@^6.0.0":
version "6.2.2"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.2.2.tgz#5c27df09ca39e3c9beb4fae6b95f4d71426df0a9"
Expand Down

0 comments on commit 4d1fc93

Please sign in to comment.