Created
August 29, 2024 20:16
-
-
Save RedHatter/8ad7826976d2f4ac553758d5b314c74d to your computer and use it in GitHub Desktop.
A simple react form handling hook
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 { DependencyList, useMemo, useRef } from 'react' | |
import * as R from 'remeda' | |
/** | |
* `useDeepMemo` will only recompute the memoized value when one of the | |
* `dependencies` has changed by value. | |
* | |
* Warning: `useDeepMemo` should not be used with dependencies that | |
* are all primitive values. Use `React.useMemo` instead. | |
* | |
* Is this a hack? Yes. Why do we do it this way? Because react hooks | |
* are really dumb and don't provide real rective state. What's the | |
* alternative? Use a real atomic state library. | |
* | |
* @see {@link https://react.dev/reference/react/useMemo} | |
*/ | |
const useDeepMemo = <T>(factory: () => T, dependencies: DependencyList) => { | |
const ref = useRef<React.DependencyList>(dependencies) | |
if (!R.equals(dependencies, ref.current)) { | |
ref.current = dependencies | |
} | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
return useMemo(factory, [ref.current]) | |
} | |
export default useDeepMemo |
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 { SyntheticEvent, useState } from 'react' | |
import * as R from 'remeda' | |
import useDeepMemo from './useDeepMemo' | |
export const validators = { | |
required: (value: unknown) => !R.isNil(value) && value !== '', | |
} | |
export type Field<V, K extends string | number | symbol = string> = { | |
value: V | |
name?: K | |
onChange?: (value: V) => void | |
disabled?: boolean | |
} | |
const useForm = <T extends object>( | |
defaultValues: T, | |
options?: { | |
onSubmit?: (values: T) => Promise<unknown> | unknown | |
disabled?: boolean | |
rules?: { [K in keyof T]?: Array<(value: T[K]) => boolean> | ((value: T[K]) => boolean) } | |
}, | |
) => { | |
const [isSubmitting, setSubmitting] = useState(false) | |
const [values, setValues] = useState({}) | |
return useDeepMemo(() => { | |
const currentValues = { | |
...defaultValues, | |
...values, | |
} | |
return { | |
fields: R.mapValues(currentValues, (value, name) => ({ | |
value, | |
name, | |
disabled: options?.disabled || isSubmitting, | |
onChange: (newValue: typeof value) => setValues((values) => ({ ...values, [name]: newValue })), | |
})) as unknown as { [K in keyof T]: Field<T[K], K> }, | |
formState: { | |
values: currentValues, | |
defaultValues, | |
isDirty: !R.isEmpty(values), | |
isSubmitting, | |
isValid: | |
options?.rules ? | |
R.toPairs(options.rules).every(([name, fn]) => | |
R.isArray(fn) ? fn.every((fn: any) => fn(currentValues[name])) : (fn as any)(currentValues[name]), | |
) | |
: true, | |
}, | |
setValues, | |
handleSubmit: (e: Event | SyntheticEvent) => { | |
e.preventDefault() | |
e.stopPropagation() | |
const res = options?.onSubmit?.(currentValues) | |
if (res instanceof Promise) { | |
setSubmitting(true) | |
res.finally(() => setSubmitting(false)).catch((e) => console.error('Error in form submit', e)) | |
} | |
return res | |
}, | |
} | |
}, [isSubmitting, options, values, defaultValues]) | |
} | |
export default useForm |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment