Created
June 18, 2019 19:39
-
-
Save alitaheri/32ac56575045d6e498008f73e231fccd to your computer and use it in GitHub Desktop.
Type safe react-final-form-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 { useCallback, useRef, useState, useEffect, useMemo } from 'react'; | |
import { | |
FormApi, | |
FormState, | |
FieldState, | |
Config, | |
FormSubscription, | |
FieldSubscription, | |
FieldValidator, | |
formSubscriptionItems, | |
fieldSubscriptionItems, | |
createForm, | |
configOptions, | |
} from 'final-form'; | |
// based on https://github.com/final-form/react-final-form-hooks/tree/master/src | |
export const allFormSubscriptions: FormSubscription = formSubscriptionItems.reduce((result, key) => { | |
(result as any)[key] = true; | |
return result; | |
}, {} as FormSubscription); | |
const subscriptionToFormInputs = (subscription: FormSubscription) => formSubscriptionItems.map(key => Boolean((subscription as any)[key as any])); | |
export function useFormState<T>(form: FormApi<T>, subscription = allFormSubscriptions) { | |
const [state, setState] = useState(() => form.getState()); | |
const deps = subscriptionToFormInputs(subscription); | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
useEffect(() => form.subscribe(setState, subscription), [form, ...deps]); | |
return state; | |
} | |
// https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily | |
function useMemoOnce<T>(factory: () => T): T { | |
const ref = useRef<T>(); | |
if (!ref.current) { | |
ref.current = factory(); | |
} | |
return ref.current; | |
} | |
export type UseFormResult<T> = FormState<T> & { | |
form: FormApi<T>; | |
handleSubmit: (event?: any) => Promise<T | undefined> | undefined; | |
} | |
export function useForm<T>({ subscription, ...config }: Config<T> & { subscription?: FormSubscription }): UseFormResult<T> { | |
const form = useMemoOnce(() => createForm(config)); | |
const prevConfig = useRef(config); | |
const state = useFormState(form, subscription); | |
const handleSubmit = useCallback(event => { | |
if (event) { | |
if (typeof event.preventDefault === 'function') { | |
event.preventDefault(); | |
} | |
if (typeof event.stopPropagation === 'function') { | |
event.stopPropagation(); | |
} | |
} | |
return form.submit(); | |
}, [form]); | |
useEffect(() => { | |
if (config === prevConfig.current) { | |
return; | |
} | |
configOptions.forEach(key => { | |
if (key !== 'initialValues' && config[key] !== prevConfig.current[key]) { | |
form.setConfig(key, config[key]); | |
} | |
}); | |
prevConfig.current = config; | |
}); | |
return { ...state, form, handleSubmit }; | |
} | |
export const allFieldSubscriptions: FieldSubscription = fieldSubscriptionItems.reduce((result, key) => { | |
(result as any)[key] = true; | |
return result; | |
}, {}); | |
const subscriptionToFieldInputs = (subscription: FieldSubscription) => fieldSubscriptionItems.map(key => Boolean((subscription as any)[key])); | |
const fieldMetaPropertyNames = fieldSubscriptionItems.filter(subscription => subscription !== 'value'); | |
function getEventValue(event: any) { | |
if (!event || !event.target) { | |
return event; | |
} else if (event.target.type === 'checkbox') { | |
return event.target.checked; | |
} | |
return event.target.value; | |
} | |
export type UseFieldMetaProps<T> = Omit<FieldState<T>, 'name' | 'blur' | 'change' | 'focus' | 'value'> | |
export interface UseFieldInputProps<T> { | |
name: string; | |
value: T; | |
onBlur: () => void; | |
onChange: (event: any) => void; | |
onFocus: () => void; | |
} | |
export interface FieldProps<T> { | |
input: UseFieldInputProps<T | undefined>; | |
meta: UseFieldMetaProps<T>; | |
} | |
export function useField<T, K extends keyof T>(form: FormApi<T>, name: K, validate?: FieldValidator<T[K]>, subscription = allFieldSubscriptions): FieldProps<T[K]> { | |
const autoFocus = useRef(false); | |
const [state, setState] = useState<FieldState<T[K]>>({} as FieldState<T[K]>); | |
const deps = subscriptionToFieldInputs(subscription); | |
useEffect(() => form.registerField( | |
name as string, | |
newState => { | |
if (autoFocus.current) { | |
autoFocus.current = false; | |
setTimeout(() => newState.focus()); | |
} | |
setState(newState); | |
}, | |
subscription, | |
validate ? { getValidator: () => validate } : undefined, | |
), [name, form, validate, ...deps]); // eslint-disable-line react-hooks/exhaustive-deps | |
const { value: fieldValue, blur, change, focus } = state; | |
const value = (fieldValue === undefined ? '' : fieldValue) as T[K]; | |
const onBlur = useCallback(() => blur(), [blur]); | |
const onChange = useCallback(event => change(getEventValue(event)), [change]); | |
const onFocus = useCallback(() => { | |
if (focus) { | |
focus(); | |
} else { | |
autoFocus.current = true; | |
} | |
}, [focus]); | |
const input = useMemo(() => ({ name, value, onBlur, onChange, onFocus }), [name, value, onBlur, onChange, onFocus]); | |
const meta = useMemo(() => { | |
const { name, value, blur, change, focus, ...meta } = state; | |
return meta; | |
}, [...fieldMetaPropertyNames.map(prop => (state as any)[prop])]); // eslint-disable-line react-hooks/exhaustive-deps | |
return useMemo(() => ({ input, meta }), [input, meta]) as FieldProps<any>; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment