0

I have this simple component that checks if username is valid. It does so by querying firebase when the input value changes. There is one problem with it. When I am typing too fast into the input field, the value in it just doesn't have enough time to change, so it just misses some characters. Here is the code:

For state management I am using Recoil.JS.

Component code:

export const UsernameInput = (props: {
  topLabel: string;
  bottomLabel?: string;
  placeholder?: string;
  className?: string;
  valueIn: any;
  valueOut: any;
  valid: any;
  validIn: boolean;
}) => {
  const usernameRef = db.collection("usernames");
  const query = usernameRef.where("username", "==", props.valueIn);

  useEffect(() => {
    query
      .get()
      .then((querySnapshot) => {
        if (querySnapshot.size >= 1) {
          props.valid(false);
        } else {
          props.valid(true);
        }
      })
  }, [props.valueIn]);

  function handleChange(event: any) {
    props.valueOut(event.target.value);
  }

  return (
    <InputSkeleton
      topLabel={props.topLabel}
      bottomLabel={props.bottomLabel}
      className={props.className}
    >
      <div className="input-username">
        <input type="text" onChange={handleChange} value={props.valueIn} />
        <span className="text">
          <span className={props.validIn ? "available" : "taken"}></span>
          {props.validIn ? "Available" : "Taken"}
        </span>
      </div>
    </InputSkeleton>
  );
};
<UsernameInput
  className="stretch"
  topLabel="Username"
  valueIn={formD.username}
  valueOut={(value: string) => {
    setFormD({ ...formD, username: value });
  }}
  valid={(value: boolean) => {
    setFormD({ ...formD, usernameValid: value });
  }}
  validIn={formD.usernameValid}
  bottomLabel="This will be your unique handle on xyz.com"
/>

3 Answers3

0

Create a simple debounce function that takes a function and time in secs as parameters:

export function debounce(func, wait) {
    let timeout;

    return function executedFunction(...args) {
        const later = () => {
            timeout = null;

            func(...args);
        };
        clearTimeout(timeout);

        timeout = setTimeout(later, wait);
    };
}

Then use it in your event handler handleChange function:

function handleChange(event: any) {
   event.preventDefault();
   // This means that you want to update the value after 500 milliseconds, i.e when you're sure that the user has stopped typing. You can extend this time to whatever figure you want
   debounce(props.valueOut(event.target.value), 500);
}
Princewill Iroka
  • 536
  • 1
  • 7
  • 17
0

Put this variable outside UsernameInput function

const WAIT_INTERVAL = 1000;

Edit your handleChange to this

componentWillMount() {
    this.timer = null;
}

function handleChange(event: any) {
    clearTimeout(this.timer);
    this.timer = setTimeout(props.valueOut(event.target.value), WAIT_INTERVAL);
}
wisnuaryadipa
  • 710
  • 1
  • 10
  • 18
0

Princewill's idea is the right one, but the implementation needs a little tweaking. Specifically, you need the timer handle to be preserved across multiple invocations of debounce, and the argument to debounce needs to be an actual function. Using a plain function doesn't do it, because each invocation results in a different local timeout handle, and the old handle never gets cancelled or updated.

I recommend adapting or using the useDebounce hook from useHooks. This uses useEffect to exploit React's effect unmounting to clear any previously-set timeouts, and is pretty clear overall.

const { valueIn, valueOut } = props;
const [username, setUsername] = useState<string>(valueIn);
// On each event, update `username`
const handleChange = useCallback(
  (event: any) => setUsername(event.target.value),
  [setUsername]
);

// Collect changes to username and change debouncedUsername to the latest
// value after a change has not been made for 500ms.
const debouncedUsername = useDebounce(username, 500);

// Each time debouncedUsername changes, run the desired callback
useEffect(() => {
  if (debouncedUsername !== valueIn) {
    valueOut(debouncedUsername);
  }
}, [valueIn, valueOut, debouncedUsername]);

The idea here is:

  1. You keep a realtime-updated copy of the field state via useState
  2. You keep a delay-updated copy of the field state via useDebounce
  3. When the delay-updated copy is finally changed, the useEffect fires your valueOut callback. As constructed, this would fire after username has changed, but has not changed again for 500ms.

Additionally, you would want to set your field's value to username, rather than valueIn, so that the field is updated in realtime, rather than on the delay.

Chris Heald
  • 61,439
  • 10
  • 123
  • 137