0

I made a custom ReactJS hook to handle a couple of specific mouse events, as below:

const HealthcareServices = ({
  filterToRemove,
  filters,
  onChange,
  onClear,
  selectedAmbulatoryCareFilterValue,
  shouldClear,
}: Props): JSX.Element => {
  const classes = useStyles();
  ...

  useEffect(() => {
    shouldClear && clearFilters();
  }, [shouldClear]);

  const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
    const [click, setClick] = useState(0);

    useEffect(() => {
      const timer = setTimeout(() => {
        // simple click
        if (click === 1) actionSimpleClick();
        setClick(0);
      }, delay);

      // the duration between this click and the previous one
      // is less than the value of delay = double-click
      if (click === 2) actionDoubleClick();

      return () => clearTimeout(timer);
    }, [click]);

    return () => setClick((prev) => prev + 1);
  };

  const handleSelectedItem = (service: Filter) => {
    service.selected = !service.selected;
    setHealthcareServices([...healthcareServices]);
    onChange(healthcareServices);
  };

  const handleSingleClick = (service: Filter) => {
    console.log('single-click');
    if (service.isRequired) {
      service.checkedIcon = <Icons.CheckboxSingleClick />;
    }
    handleSelectedItem(service);
  };

  const handleDoubleClick = (service: Filter) => {
    console.log('double-click');
    if (service.isRequired) {
      service.checkedIcon = <Icons.CheckboxDoubleClick />;
    }
    handleSelectedItem(service);
  };

  const handleClick = (service: Filter) =>
    useSingleAndDoubleClick(
      () => handleSingleClick(service),
      () => handleDoubleClick(service)
    );
  ...

  return (
    <div className={classes.filter_container}>
      ...
      <div className={classes.filter_subgroup}>
        {filters.map((filter) => (
          <div key={`${filter.label}-${filter.value}`} className={classes.filter}>
            <Checkbox
              label={filter.label}
              className={classes.checkbox}
              checked={filter.selected}
              onChange={() => handleClick(filter)}
              checkedIcon={filter.checkedIcon}
            />
          </div>
        ))}
      </div>
      ...
    </div>
  );
};

When I click on my <Checkbox />, the whole thing crashes. The error is:

The top of my stacktrace points to useState inside my hook. If I move it outside, so the hook looks as:

const [click, setClick] = useState(0);

const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {

    useEffect(() => {
      const timer = setTimeout(() => {
        // simple click
        if (click === 1) actionSimpleClick();
        setClick(0);
      }, delay);

      // the duration between this click and the previous one
      // is less than the value of delay = double-click
      if (click === 2) actionDoubleClick();

      return () => clearTimeout(timer);
    }, [click]);

    return () => setClick((prev) => prev + 1);
  };

The problem still happens, only the stacktrace points to the useEffect hook. The code is based on another answer here.

Any suggestions?

Igor Shmukler
  • 1,742
  • 3
  • 15
  • 48

1 Answers1

-1

You've defined your useSingleAndDoubleClick hook inside of a component. That's not what you want to do. The idea of custom hooks is that you can move logic outside of your components that could otherwise only happen inside of them. This helps with code reuse.

There is no use for a hook being defined inside a function, as the magic of hooks is that they give you access to state variables and such things that are usually only allowed to be interacted with inside function components.

You either need to define your hook outside the component and call it inside the component, or remove the definition of useSingleAndDoubleClick and just do everything inside the component.

EDIT: One more note to help clarify: the rule that you've really broken here is that you've called other hooks (ie, useState, useEffect) inside your useSingleAndDoubleClick function. Even though it's called useSingleAndDoubleClick, it's not actually a hook, because it's not being created or called like a hook. Therefore, you are not allowed to call other hooks inside of it.

EDIT: I mentioned this earlier, but here's an example that could work of moving the hook definition outside the function:

EDIT: Also had to change where you call the hook: you can't call the hook in a nested function, but I don't think you need to.

const useSingleAndDoubleClick = (actionSimpleClick: () => void, actionDoubleClick: () => void, delay = 250) => {
    const [click, setClick] = useState(0);

    useEffect(() => {
      const timer = setTimeout(() => {
        // simple click
        if (click === 1) actionSimpleClick();
        setClick(0);
      }, delay);

      // the duration between this click and the previous one
      // is less than the value of delay = double-click
      if (click === 2) actionDoubleClick();

      return () => clearTimeout(timer);
    }, [click]);

    return () => setClick((prev) => prev + 1);
  };

const HealthcareServices = ({
  filterToRemove,
  filters,
  onChange,
  onClear,
  selectedAmbulatoryCareFilterValue,
  shouldClear,
}: Props): JSX.Element => {
  const classes = useStyles();
  ...

  useEffect(() => {
    shouldClear && clearFilters();
  }, [shouldClear]);
  // your other handlers

  // changed this - don't call the hook inside the function.
  // your hook is returning the handler you want anyways, I think
  const handleClick = useSingleAndDoubleClick(handleSingleClick, handleDoubleClick)
Willow
  • 1,132
  • 5
  • 20
  • Having the hook code inside is not the problem. I thought that it was clear from the post. The code crashes and stack traces point to nested hooks. I am looking for ways to code this correctly. – Igor Shmukler Feb 08 '22 at 11:55
  • Hmm, according to https://reactjs.org/docs/hooks-rules.html, you can only call hooks at the top level. But you're not doing that. You're calling a hook inside a nested function which is explicitly forbidden. – Willow Feb 08 '22 at 13:43
  • Edited my answer with an example that I think will do what you're looking for. Hope this helps :) – Willow Feb 08 '22 at 13:57