Skip to content

Instantly share code, notes, and snippets.

@sibelius
Last active May 10, 2023 04:07
Show Gist options
  • Save sibelius/ccea4d505eb20d0dd08c19716c469093 to your computer and use it in GitHub Desktop.
Save sibelius/ccea4d505eb20d0dd08c19716c469093 to your computer and use it in GitHub Desktop.
useFastField that uses local state `onChange` and sync back to formik only `onBlur`
import React, { useState, useEffect } from 'react';
import { useField, FieldHookConfig, FieldInputProps, FieldMetaProps, FieldHelperProps } from 'formik';
import { useDebouncedCallback } from 'use-debounce';
const DEBOUNCE_DELAY = 300;
export function useFastField<Val = any>(
propsOrFieldName: string | FieldHookConfig<Val>,
): [FieldInputProps<Val>, FieldMetaProps<Val>, FieldHelperProps<Val>] {
const [field, meta, helpers] = useField<Val>(propsOrFieldName);
const [value, setValue] = useState<Val>(field.value);
const { onBlur, onChange } = field;
const { setValue: helpersSetValue } = helpers;
const { shouldResync = true } = propsOrFieldName;
const currentFieldValue = field.value;
// resync field.value
useEffect(() => {
if (shouldResync) {
if (currentFieldValue !== value) {
setValue(currentFieldValue);
}
}
}, [currentFieldValue, shouldResync]);
const [onSync] = useDebouncedCallback((e) => {
onChange(e);
onBlur(e);
}, DEBOUNCE_DELAY);
const [onSyncValue] = useDebouncedCallback((val: Val) => {
helpersSetValue(val);
helpers.setTouched(true);
}, DEBOUNCE_DELAY);
// monkey patch formik field
field.value = value;
field.onChange = (e: React.ChangeEvent<any>): void => {
if (e && e.currentTarget) {
setValue(e.currentTarget.value);
}
onSync(e);
};
field.onBlur = (e: any): void => {
onChange(e);
onBlur(e);
};
helpers.setValue = (val: Val) => {
setValue(val);
onSyncValue(val);
};
return [field, meta, helpers];
}
@taiwanhua
Copy link

I made a change to trigger onChange when stop typing

import { useState, useEffect } from "react";
import { useField } from "formik";

export function useFastField(propsOrFieldName) {
  const INTERVAL = 250;

  const [field, meta, helpers] = useField(propsOrFieldName);
  const [value, setValue] = useState(field.value);
  const { onBlur, onChange } = field;
  const [fieldChangeEvent, setFieldChangeEvent] = useState();

  useEffect(() => {
    const timeout = setTimeout(() => {
      if (fieldChangeEvent && fieldChangeEvent?.target) {
        onChange(fieldChangeEvent);
      }
    }, INTERVAL);

    return () => clearTimeout(timeout);
  }, [fieldChangeEvent, onChange]);

  field.value = value;

  field.onChange = e => {
    if (e && e.target) {
      setValue(e.currentTarget.value);

      e.persist();
      setFieldChangeEvent(e);
    }
  };

  field.onBlur = e => {
    onChange(e);
    onBlur(e);
  };

  helpers.setValue = value => {
    setValue(value);
  };

  return [field, meta, helpers];
}

this will have bug when just use helper.setValue,
it should call onChange ,or the value won't be update

@taiwanhua
Copy link

taiwanhua commented Apr 21, 2022

if you will set field by other field (with setFieldValue from useFormikContext)
you can use like below

import {
  useFormikContext,
  useField as useFormikField,
  FieldInputProps,
  FieldMetaProps,
  FieldHelperProps as FormikFieldHelperProps,
} from "formik";
import { get, isEqual } from "lodash";
import {
  useCallback,
  useState,
  useEffect,
  ChangeEvent,
  FocusEvent,
} from "react";

export interface FieldHelperProps<T>
  extends Omit<FormikFieldHelperProps<T>, "setValue"> {
  setValue: (
    value: T,
    isCrossFieldSetValue?: boolean | undefined,
    shouldValidate?: boolean | undefined,
  ) => void;
}

export function useField<Value>(
  name: string,
): [FieldInputProps<Value>, FieldMetaProps<Value>, FieldHelperProps<Value>] {
  const interval = 300;

  const [field, meta, helper] = useFormikField<Value>(name);

  const [formikFieldValue, setFormikFieldValue] = useState(field.value);
  const { values, setFieldValue } = useFormikContext();

  const { onBlur, onChange } = field;

  const [fieldChangeEvent, setFieldChangeEvent] =
    useState<ChangeEvent<unknown> | null>(null);

  useEffect(() => {
    const timeout = setTimeout(() => {
      if (fieldChangeEvent && fieldChangeEvent?.target) {
        onChange(fieldChangeEvent);
      }
    }, interval);

    return () => clearTimeout(timeout);
  }, [fieldChangeEvent, onChange]);

  field.onChange = useCallback((e: ChangeEvent<unknown>) => {
    if (e && e.target) {
      const target = e.target as HTMLInputElement;

      setFormikFieldValue(target.value as unknown as Value);

      setFieldChangeEvent(e);
    }
  }, []);

  field.onBlur = useCallback(
    (e: FocusEvent<unknown, Element>) => {
      onChange(e);
      onBlur(e);
    },
    [onBlur, onChange],
  );

  helper.setValue = useCallback(
    (
      nextFieldValue: Value,
      isCrossFieldSetValue = false,
      shouldValidate?: boolean,
    ) => {
      setFormikFieldValue(nextFieldValue);
      if (isCrossFieldSetValue) {
        setFieldValue(name, nextFieldValue, shouldValidate);
      }
    },
    [name, setFieldValue],
  );

  field.value = formikFieldValue;

  // when values was updated in formik context, it should replace formikFieldValue by newest value
  useEffect(() => {
    if (isEqual(formikFieldValue, get(values, name))) {
      return;
    }
    setFormikFieldValue(get(values, name));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [name, values]);

  return [field, meta, helper];
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment