Skip to content

Instantly share code, notes, and snippets.

@jtomaszewski
Last active November 8, 2023 11:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jtomaszewski/519781e5d2a585bbdbc6f8b76bad7f43 to your computer and use it in GitHub Desktop.
Save jtomaszewski/519781e5d2a585bbdbc6f8b76bad7f43 to your computer and use it in GitHub Desktop.
Hook for `react-hook-form` that lets you maintain state of a form field's warning (that is: an error that doesn't prohibit the form from being submitted).
import { useCallback, useState, useEffect, useRef } from "react";
import {
LiteralToPrimitive,
UnpackNestedValue,
useFormContext
} from "react-hook-form";
import { useDebouncedCallback } from "use-debounce";
/**
* Returns state of a warning for the given form field.
*
* Temporary workaround until `react-hook-form`
* [supports warnings](https://github.com/react-hook-form/react-hook-form/issues/1761).
*
* @example
* ```tsx
* const { warning, triggerWarning } = useFormFieldWarning<
* FormData,
* 'ccNumber',
* string | undefined
* >({
* name: 'ccNumber',
* validate(value) {
* if (value && !validateCcNumber(value)) {
* return "This number looks incorrect. Please double check it before submitting.";
* }
* }
* });
*
* return (
* <>
* <Controller
* name="ccNumber"
* control={control}
* render={({ onChange, onBlur, value }): React.ReactElement => (
* <TextInput
* value={value}
* onChangeText={onChange}
* onBlur={(): void => {
* onBlur();
* triggerWarning();
* }}
* />
* )}
* />
* {warning && <Alert type="warning">{warning}</Alert>}
* </>
* );
* ```
*
* @license WTFPL (http://www.wtfpl.net/txt/copying)
* @see https://gist.github.com/jtomaszewski/519781e5d2a585bbdbc6f8b76bad7f43
*/
export function useFormFieldWarning<
TFieldValues extends Record<string, any>,
TFieldName extends string,
TFieldValue
>({
name,
validate: validateValue,
onChangeDebounceDelay = 500
}: {
/**
* For now only one mode is supported,
* but maybe we'll want to have more in the future?
*
* - `onChangeDebounced`: after each change, warning is emptied immediately,
* and then validated after `onChangeDebounceDelay` ms.
*
* You should also call `triggerWarning()` in your field's `onBlur()` so that it is
* validated immediately after it.
*/
mode?: "onChangeDebounced";
name: TFieldName;
validate(
value: TFieldName extends keyof TFieldValues
? UnpackNestedValue<TFieldValues[TFieldName]>
: UnpackNestedValue<LiteralToPrimitive<TFieldValue>>
): string | undefined;
onChangeDebounceDelay?: number;
}): {
warning: string | undefined;
triggerWarning(): void;
} {
const [warning, setWarning] = useState<string>();
const validateValueRef = useRef(validateValue);
validateValueRef.current = validateValue;
const { watch, getValues } = useFormContext<TFieldValues>();
const validate = useCallback((): void => {
const value = getValues<TFieldName, TFieldValue>(name);
const nextWarning = validateValueRef.current(value as any);
setWarning(nextWarning);
}, [getValues, name]);
const {
callback: validateDebounced,
cancel: cancelValidateDebounce
} = useDebouncedCallback(validate, onChangeDebounceDelay);
const value = watch<TFieldName, TFieldValue>(name);
useEffect((): void => {
setWarning(undefined);
cancelValidateDebounce();
validateDebounced();
}, [value, cancelValidateDebounce, validateDebounced]);
return {
warning,
triggerWarning: validate
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment