'React: Perform action, only once, when html element is clicked

In jQuery, it's pretty trivial to perform an action, only once, when an event is triggered on the html element.

For example:

$(".dropdown-trigger").on("click", function(event) {
  var self = this;
  $("html").one("click", function() {
    $(self).parents(".dropdown").removeClass("active");
  });
  $(this).parents(".dropdown").toggleClass("active");

  event.stopPropagation();
});

In React, it's not so easy.

How can I perform an action, in React, when the html element is clicked, and perform that action only once (similar to my jQuery code)?



Solution 1:[1]

Using refs. Not very beatiful but at least it works. This requires React >16.8

import React, { useEffect, useRef } from 'react'

const App = () => {
  const ref = useRef()

  // Wait for DOM to load
  useEffect(() => ref.addEventListener('click', () => alert('Hi'), { once: true })

  return <button ref={ref}>Click on me (once)</button>
}

Solution 2:[2]

With useState, also for React > 16.8

import React, { useState } from 'react';

const App = () => {
  const [clicked, setClicked] = useState(false);
  const myFunc = () => alert('hey');
  const onClick = (event) => {
    if (!clicked) {
      setClicked(true);
      myFunc(event);
    }
  };
  return <button onClick={onClick}>Click on me (once)</button>;
};

Solution 3:[3]

Flip the state once the element has triggered its event, react will re-render the component since your state changed and it won't be displayed. Below is an example of one way to achieve that outcome.

_onEvent() {
    this.setState({
        hasTriggered: true
    })
}


render() {
    return (
        <div>
            {
                !this.state.hasTriggered ?
                <MyElement onClick={this._onEvent.bind(this)} /> : null
            }
        </div>
    )
}

Solution 4:[4]

Functional programming's higher order method

Ref:

https://betterprogramming.pub/how-to-allow-only-one-click-events-in-javascript-72938027fbf5

Gist:

const once = (f) => {
  let finished = false;
  return (...args) => {
    if (!finished) {
      finished = true;
      f(...args);
    }
  };
};

Complete example:

  • clicker 0 will fire events on every click.
    • this is the problematic one.
    • this can cause extra triggers to server-side if the network is slow and the user clicks again hoping the data will be submitted faster.
  • clicker 1 will only fire again, after the button itself and the click function ref is re-rendered.
    • this is a simple solution if we have to re-render the main component after the server has responded.
  • clicker 2 will only fire again when value2 has any changes.
    • if we want to control using specific state
import "./App.css";
import { useState, useCallback } from "react";

const once = (f) => {
  let finished = false;
  return (...args) => {
    if (!finished) {
      finished = true;
      f(...args);
    }
  };
};

function App() {
  const [value1, setValue1] = useState("value1");
  const [value2, setValue2] = useState("value2");

  console.log(`app rendered`);

  const onChange1 = useCallback((e) => {
    console.log(`onChange1`, e.target.value);
    setValue1(e.target.value);
  }, []);

  const onChange2 = useCallback((e) => {
    console.log(`onChange2`, e.target.value);
    setValue2(e.target.value);
  }, []);

  const onClick0 = () => {
    // mocking 2 secs network request
    setTimeout(() => {
      // set value to value1 to cause the re-render
      setValue1(new Date());
      console.log("clicker 0");
    }, 2000);
  };

  const onClick1 = () => {
    // mocking 2 secs network request
    setTimeout(() => {
      // set value to value1 to cause the re-render
      setValue1(new Date());
      console.log("clicker 1");
    }, 2000);
  };

  const onClick2 = () => {
    // mocking 2 secs network request
    setTimeout(() => {
      // set value to value1 to cause the re-render
      setValue1(new Date());
      console.log("clicker 2");
    }, 2000);
  };

  const memoOnceOnClick2 = useCallback(once(onClick2), [value2]);

  return (
    <div className="App">
      <header className="App-header">
        <input value={value1} onChange={onChange1} />
        <input value={value2} onChange={onChange2} />
        <button onClick={onClick0}>
          clicker 0 / run every time i am clicked
        </button>
        <button onClick={once(onClick1)}>
          clicker 1 / run once until i am re-render
        </button>
        <button onClick={memoOnceOnClick2}>
          clicker 2 / run once until value2 is changed
        </button>
      </header>
    </div>
  );
}

export default App;

Solution 5:[5]

This is a much simpler solution, in my opinion:

<div
  className={`tile ${tile.marked ? "team1" : ""}`}
  onClick={!tile.marked ? () => markTile(tile.id) : null}
></div>

This is with React 18. As you can see, I'm checking if I've already clicked it by checking if the marked property is false. Only then do I call my function.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Paul Losev
Solution 2 laurens
Solution 3 Jack
Solution 4 Aung
Solution 5 LAGS