Last active
March 12, 2021 10:58
-
-
Save tgriesser/fdd08da075fec80328792319606c76e5 to your computer and use it in GitHub Desktop.
Formik w/ Hooks
This file contains 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>; | |
} |
I also had a stab at the implementation but didn't use useRef
or useCallback
once. Curious if you think I should have used those and why you decided to use them. Here is a link to my attempt. I would really appreciate a quick review if you get the chance. Thanks
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Curious why you used
useRef
. I attempted to use this code but it wasn't setting the fields on change.Would I have to do something like the following on the client side?
value={values.current ? values.current['email'] : values['email']}