jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
33.87k stars 2.79k forks source link

Unexpected behaviour with Yup's `oneOf()` #2403

Open mgansler opened 4 years ago

mgansler commented 4 years ago

🐛 Bug report

Current Behavior

Error message only shows when manually clicking somewhere else (blur)

Expected Behavior

Error message appears as soon as the third option is selected

Reproducible example

import React from 'react'
import { Formik, useField } from 'formik'
import { noop } from 'lodash'
import * as Yup from 'yup'

interface DummyProps {
    fieldName: string
    options: { value: string; label: string }[]
}

const Dummy: React.FC<DummyProps> = ({ fieldName, options }) => {
    const [field, meta, { setTouched, setValue }] = useField({
        name: fieldName,
    })

    console.log(meta.touched, meta.error)

    return (
        <React.Fragment>
            {options.map(({ value }) => (
                <input
                    {...field}
                    type={'radio'}
                    value={value}
                    checked={field.value === value}
                    onChange={() => {
                        setTouched(true)
                        setValue(value)
                    }}
                />
            ))}
            <span>Error: {meta.error}</span>
        </React.Fragment>
    )
}

export default {
    title: 'Test',
    component: Dummy,
}

const validationSchema = Yup.object({
    test: Yup.mixed().oneOf(['one', 'two']),
    // test: Yup.string().max(3),
})

export const Story = () => {
    return (
        <Formik
            initialValues={{ test: 'one' }}
            onSubmit={noop}
            validationSchema={validationSchema}
            initialTouched={{ test: true }}
            validateOnChange={true}
        >
            <form>
                <Dummy
                    fieldName={'test'}
                    options={[
                        { value: 'one', label: 'One' },
                        { value: 'two', label: 'Two' },
                        { value: 'three', label: 'Three' },
                    ]}
                />
            </form>
        </Formik>
    )
}

Suggested solution(s)

Maybe related: setValue and setTouched seem to be a new reference each render iteration. Wrapping them in useCallback may help.

Additional context

Debugging showed that with oneOf(), the error message appears for a short time but goes away immediately because the validation is called again with the initial value (which is valid). But that doesn't seem to happen when using some other form of validation. e.g. string().max().

Your environment

Software Version(s)
Formik 2.1.4
React 16.13.1
TypeScript 3.8.3
Browser Chrome (but doesn't matter)
npm/Yarn Yarn 1.22.4
Operating System macos (but doesn't matter
yup 0.28.3
coreyar commented 4 years ago

What I think is happening is that setTouched and setValue are updating state asynchronously and the setTouched action doesn't have the state with the error and is thus clearing it. Both hooks are using useCallback. One solution may be to use useEffect to update state synchronously.

You could move setTouched to onFocus.

It also looks like the <Formik/> component implements these functions imperatively.