Skip to content

Instantly share code, notes, and snippets.

@tgriesser
Last active March 12, 2021 10:58
Show Gist options
  • Save tgriesser/fdd08da075fec80328792319606c76e5 to your computer and use it in GitHub Desktop.
Save tgriesser/fdd08da075fec80328792319606c76e5 to your computer and use it in GitHub Desktop.
Formik w/ Hooks
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>;
}
@mfbx9da4
Copy link

mfbx9da4 commented May 11, 2019

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']}

function LoginForm() {
  const initialValues: LoginVariables = {
    password: 'test',
    email: 'test@gmail.com'
  }
  const [login, { data, error, loading }] = loginMutation()
  const config = {
    initialValues,
    onSubmit: async (values: LoginVariables) => {
      await login({ variables: values })
    }
  }
  return (
    <Formik {...config}>
      <FormikContext.Consumer>
        {context => {
          if (!context) return null
          const {
            values,
            setFieldValue,
            setFieldTouched,
            handleSubmit
          } = context
          return (
            <View>
              <View>
                <TextInput
                  value={values['email']}
                  placeholder="Email"
                  onChangeText={value => setFieldValue('email', value)}
                  onBlur={value => setFieldTouched('email')}
                />
              </View>
              <View>
                <TextInput
                  value={values.password}
                  onChangeText={value => setFieldValue('password', value)}
                  onBlur={value => setFieldTouched('password')}
                  placeholder={'Password'}
                />
              </View>
              <TouchableOpacity onPress={handleSubmit}>
                <View>
                  <Text>Login</Text>
                </View>
              </TouchableOpacity>
            </View>
          )
        }}
      </FormikContext.Consumer>
    </Formik>
  )
}

@mfbx9da4
Copy link

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