Skip to content

Instantly share code, notes, and snippets.

@alitaheri
Created June 18, 2019 19:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save alitaheri/32ac56575045d6e498008f73e231fccd to your computer and use it in GitHub Desktop.
Save alitaheri/32ac56575045d6e498008f73e231fccd to your computer and use it in GitHub Desktop.
Type safe react-final-form-hooks
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