Skip to content

Instantly share code, notes, and snippets.

@RedHatter
Created August 29, 2024 20:16
Show Gist options
  • Save RedHatter/8ad7826976d2f4ac553758d5b314c74d to your computer and use it in GitHub Desktop.
Save RedHatter/8ad7826976d2f4ac553758d5b314c74d to your computer and use it in GitHub Desktop.
A simple react form handling hook
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
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