Skip to content

Commit

Permalink
Validate setFieldTouched with high priority. (jaredpalmer#2882)
Browse files Browse the repository at this point in the history
Yet another follow up fix for the jaredpalmer#2841.

In 2.2.0 `validateFormWithLowPriority` was immideatly called by `unstable_runWithPriority`
https://github.com/formium/formik/blob/7279751518089fac6a446b8a3b9c46035b65ea23/packages/formik/src/Formik.tsx#L329-L332

But 2.2.1 it was delayed, so when we validate values only on blur, users could see the error for few milliseconds. 

Here is the repro jaredpalmer#2841 (comment)
  • Loading branch information
avocadowastaken committed Nov 10, 2020
1 parent ff758f9 commit 199e77a
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 62 deletions.
6 changes: 6 additions & 0 deletions .changeset/hungry-seals-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'formik': patch
'formik-native': patch
---

Validate `setFieldTouched` with high priority
15 changes: 14 additions & 1 deletion app/pages/sign-in.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React, { useEffect, useState } from 'react';
import { ErrorMessage, Field, Form, FormikProvider, useFormik } from 'formik';
import * as Yup from 'yup';
import { useRouter } from 'next/router';

const SignIn = () => {
const router = useRouter();
const [errorLog, setErrorLog] = useState([]);

const formik = useFormik({
validateOnMount: true,
validateOnMount: router.query.validateOnMount === 'true',
validateOnBlur: router.query.validateOnBlur !== 'false',
validateOnChange: router.query.validateOnChange !== 'false',
initialValues: { username: '', password: '' },
validationSchema: Yup.object().shape({
username: Yup.string().required('Required'),
Expand Down Expand Up @@ -69,6 +73,15 @@ const SignIn = () => {
Submit
</button>

<button
type="reset"
onClick={() => {
setErrorLog([]);
}}
>
Reset
</button>

<pre id="error-log">{JSON.stringify(errorLog, null, 2)}</pre>
</Form>
</FormikProvider>
Expand Down
36 changes: 35 additions & 1 deletion cypress/integration/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('basic validation', () => {
cy.get('#renderCounter').contains('0');
});

it('should validate show errors on blur', () => {
it('should validate show errors on change and blur', () => {
cy.visit('http://localhost:3000/sign-in');

cy.get('input[name="username"]')
Expand All @@ -33,6 +33,40 @@ describe('basic validation', () => {
cy.get('#error-log').should('have.text', '[]');
});

it('should validate show errors on blur only', () => {
cy.visit('http://localhost:3000/sign-in', {
qs: {
validateOnMount: false,
validateOnChange: false,
},
});

cy.get('input[name="username"]')
.type('john')
.blur()
.siblings('p')
.should('have.length', 0);

cy.get('input[name="password"]')
.type('123')
.blur()
.siblings('p')
.should('have.length', 0);

cy.get('#error-log').should(
'have.text',
JSON.stringify(
[
// It will quickly flash after `password` blur because `yup` schema
// validation is async.
{ name: 'password', value: '123', error: 'Required' },
],
null,
2
)
);
});

it('should validate autofill', () => {
// React overrides `input.value` setters, so we have to call
// native input setter
Expand Down
2 changes: 1 addition & 1 deletion packages/formik/src/Formik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,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 Down
58 changes: 0 additions & 58 deletions packages/formik/test/Formik.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1625,63 +1625,5 @@ describe('<Formik>', () => {

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

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

expect(validate).not.toBeCalled();

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

act(() => {
fireEvent.blur(getByRole('textbox'));
});

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

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

act(() => {
fireEvent.blur(getByRole('textbox'));
});

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 blur
[{ name: 'i' }, undefined],
// Scheduled on second blur
[{ name: 'ian' }, undefined],
]);
});

expect(renderedErrors).toHaveLength(0);
});
});
});
31 changes: 30 additions & 1 deletion packages/formik/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,33 @@ declare module 'deepmerge' {
function all<T>(objects: Array<Partial<T>>, options?: Options): T;
}
}
declare module 'scheduler';

declare module 'scheduler' {
export const unstable_NoPriority = 0;
export const unstable_ImmediatePriority = 1;
export const unstable_UserBlockingPriority = 2;
export const unstable_NormalPriority = 3;
export const unstable_LowPriority = 4;
export const unstable_IdlePriority = 5;

export function unstable_runWithPriority<T>(
priorityLevel: number,
eventHandler: () => T
): T;

export interface Task {
id: number;
}

export interface ScheduleCallbackOptions {
delay?: number;
}

export function unstable_scheduleCallback(
priorityLevel: number,
callback: () => void,
options?: ScheduleCallbackOptions
): Task;

export function unstable_cancelCallback(task: Task): void;
}

0 comments on commit 199e77a

Please sign in to comment.