-
-
Save tarex/91f5b0cffb77b5f815baf11f93ee4476 to your computer and use it in GitHub Desktop.
Formik w/ Hooks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
useContext, | |
createContext, | |
createElement, | |
useEffect, | |
useRef, | |
useCallback, | |
useState, | |
} from 'react'; | |
import isEqual from 'react-fast-compare'; | |
export type FormikErrors<V> = { | |
[K in Extract<keyof V, string>]?: V[K] extends object ? FormikErrors<V[K]> : string | |
}; | |
export type FormikTouched<V> = { | |
[K in Extract<keyof V, string>]?: V[K] extends object ? FormikTouched<V[K]> : boolean | |
}; | |
export interface FormikProps<V = any> { | |
initialValues: V; | |
validate?: ((values: V) => void | object | Promise<FormikErrors<V>>); | |
onSubmit: (values: V) => any; | |
validateOnBlur?: boolean; | |
validateOnChange?: boolean; | |
validateOnLoad?: boolean; | |
enableReinitialize?: boolean; | |
isInitialValid?: boolean | ((values: V) => boolean); | |
children: React.ReactNode; | |
} | |
export interface FormikActions<V = any> { | |
resetForm(): void; | |
submitForm(): Promise<void>; | |
validateForm(): Promise<void>; | |
setErrors(nextErrors: FormikErrors<V>): void; | |
setFieldError(field: keyof V & string, err?: string): void; | |
setFieldTouched(field: keyof V & string, isTouched?: boolean, shouldValidate?: boolean): void; | |
setFieldValue(key: keyof V & string, val: any): void; | |
setStatus(): void; | |
setSubmitting(isSubmitting: boolean): void; | |
setTouched(fields: FormikTouched<V>): void; | |
setValues(nextValues: V): void; | |
} | |
export interface FormikFieldProps { | |
field: { | |
name: string; | |
value: any; | |
onChange(e): void; | |
onBlur(): void; | |
}; | |
meta: { | |
touched: boolean; | |
error: string | undefined; | |
}; | |
} | |
export interface FormikContextShape<V = any> extends FormikActions<V> { | |
values: V; | |
initialValues: V; | |
dirty: boolean; | |
isValid: boolean; | |
errors: FormikErrors<V>; | |
touched: FormikTouched<V>; | |
submitAttemptCount: number; | |
isSubmitting: boolean; | |
isValidating: boolean; | |
validateOnChange: boolean; | |
validateOnBlur: boolean; | |
getFieldProps(name: string, type?: string): FormikFieldProps; | |
handleSubmit(e); | |
} | |
export const FormikContext = createContext<FormikContextShape | null>(null); | |
export function Formik<Values>({ | |
validate, | |
onSubmit, | |
validateOnBlur = true, | |
validateOnChange = true, | |
validateOnLoad = false, | |
enableReinitialize = false, | |
isInitialValid = false, | |
children, | |
...props | |
}: FormikProps) { | |
const initialValues = React.useRef(props.initialValues); | |
const validateOnLoadInitial = useRef(validateOnLoad); | |
const status = useRef<any>(); | |
const values = useRef(initialValues.current); | |
const isValidating = useRef<Promise<void> | false>(false); | |
const errors = useRef<FormikErrors<Values>>({}); | |
const touched = useRef<FormikTouched<Values>>({}); | |
const isSubmitting = useRef<Promise<void> | false>(false); | |
const submitAttemptCount = useRef(0); | |
const [, forceUpdate] = useState(); | |
React.useEffect( | |
() => { | |
if (enableReinitialize && !isEqual(props.initialValues, initialValues.current)) { | |
initialValues.current = props.initialValues; | |
values.current = initialValues.current; | |
forceUpdate(); | |
} | |
}, | |
[props.initialValues, enableReinitialize] | |
); | |
const dirty = React.useMemo(() => !isEqual(initialValues.current, values.current), [ | |
initialValues.current, | |
values.current, | |
]); | |
const isValid = React.useMemo( | |
() => | |
dirty | |
? errors.current && Object.keys(errors.current).length === 0 | |
: isInitialValid !== false && typeof isInitialValid === 'function' | |
? isInitialValid(initialValues.current) | |
: isInitialValid, | |
[errors.current, dirty, isInitialValid, initialValues.current] | |
); | |
const validateForm = useCallback((vals = values.current): Promise<void> => { | |
if (isValidating.current) { | |
console.warn('validateForm called while the form is already validating.'); | |
return isValidating.current; | |
} | |
isValidating.current = Promise.resolve(validate ? validate(vals) : {}) | |
.then((x) => x, (e) => e) | |
.then((e) => { | |
errors.current = e; | |
isValidating.current = false; | |
forceUpdate(); | |
}); | |
return isValidating.current; | |
}, []); | |
const getFieldProps = (name: keyof Values & string, type = 'input') => { | |
return { | |
field: { | |
value: | |
type === 'radio' || type === 'checkbox' | |
? undefined // React uses checked={} for these inputs | |
: getIn(values.current, name), | |
onChange(e: React.ChangeEvent<any>) { | |
e.preventDefault(); | |
setFieldValue(name, e.target.value); | |
}, | |
onBlur() { | |
setFieldTouched(name); | |
}, | |
}, | |
meta: { | |
touched: Boolean(getIn(touched.current, name)), | |
error: getIn(errors.current, name), | |
}, | |
}; | |
}; | |
const submitForm = () => { | |
if (isSubmitting.current) { | |
return isSubmitting.current; | |
} | |
isSubmitting.current = Promise.resolve(); | |
touched.current = setNestedObjectValues(values.current, true); | |
submitAttemptCount.current += 1; | |
forceUpdate(); | |
return validateForm() | |
.then(() => onSubmit(values)) | |
.then((val) => { | |
isSubmitting.current = false; | |
forceUpdate(); | |
return val; | |
}) | |
.catch((e) => { | |
isSubmitting.current = false; | |
forceUpdate(); | |
throw e; | |
}); | |
}; | |
const resetForm = (nextValues?: Values) => { | |
errors.current = {}; | |
touched.current = {}; | |
isSubmitting.current = false; | |
isValidating.current = false; | |
values.current = nextValues || initialValues.current; | |
forceUpdate(); | |
}; | |
const setErrors = useCallback((nextErrors: FormikErrors<Values>) => { | |
errors.current = nextErrors; | |
forceUpdate(); | |
}, []); | |
const handleSubmit = (e) => { | |
e.preventDefault(); | |
submitForm(); | |
}; | |
const setFieldValue = <K extends keyof Values & string>( | |
key: K, | |
value: K extends keyof Values ? Values[K] : any | |
) => { | |
const currentValues = values.current; | |
values.current = setIn(currentValues, key, value); | |
if (currentValues !== values.current) { | |
forceUpdate(); | |
if (validateOnChange) { | |
validateForm(); | |
} | |
} | |
}; | |
const setTouched = useCallback( | |
(fields: FormikTouched<Values>) => { | |
const currentTouched = values.current; | |
touched.current = setNestedObjectValues(fields, true); | |
if (touched.current !== currentTouched) { | |
forceUpdate(); | |
if (validateOnBlur && validate) { | |
validateForm(); | |
} | |
} | |
}, | |
[touched.current, validateOnBlur, validate] | |
); | |
const setSubmitting = useCallback((value: boolean) => { | |
if (value) { | |
if (!isSubmitting.current) { | |
isSubmitting.current = Promise.resolve(); | |
forceUpdate(); | |
} | |
} else if (isSubmitting.current) { | |
isSubmitting.current = false; | |
forceUpdate(); | |
} | |
}, []); | |
const setValues = useCallback((nextValues: Values) => { | |
values.current = nextValues; | |
forceUpdate(); | |
}, []); | |
const setStatus = useCallback((nextStatus?: any) => { | |
status.current = nextStatus; | |
forceUpdate(); | |
}, []); | |
const setFieldError = useCallback((field: keyof Values & string, errorMsg: string) => { | |
errors.current = setIn(errors.current, field, errorMsg); | |
forceUpdate(); | |
}, []); | |
const setFieldTouched = useCallback( | |
(field: keyof Values & string, isTouched: boolean = true, shouldValidate: boolean = true) => { | |
const touchedVal = touched.current; | |
touched.current = setIn(touched.current, field, isTouched); | |
forceUpdate(); | |
if (shouldValidate && touchedVal !== touched.current) { | |
validateForm(); | |
} | |
}, | |
[validate] | |
); | |
const formikActions: FormikActions<Values> = { | |
resetForm, | |
submitForm, | |
validateForm, | |
setErrors, | |
setFieldError, | |
setFieldTouched, | |
setFieldValue, | |
setStatus, | |
setSubmitting, | |
setTouched, | |
setValues, | |
}; | |
const ctx: FormikContextShape<Values> = { | |
dirty, | |
touched: touched.current, | |
initialValues: initialValues.current, | |
values: values.current, | |
errors: errors.current, | |
submitAttemptCount: submitAttemptCount.current, | |
isSubmitting: Boolean(isSubmitting.current), | |
isValidating: Boolean(isValidating.current), | |
validateOnChange, | |
validateOnBlur, | |
getFieldProps, | |
handleSubmit, | |
isValid, | |
...formikActions, | |
}; | |
useEffect( | |
() => { | |
if (validateOnLoadInitial.current) { | |
validateForm(); | |
} | |
}, | |
[validateOnLoadInitial.current] | |
); | |
return <FormikContext.Provider value={ctx}>{children}</FormikContext.Provider>; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment