The useEffect Cleanup Function

Alice Moretti   September 7th, 2022

These days I am following a really good React course on Udemy (React - The Complete Guide (incl Hooks, React Router, Redux)). It’s a great course as the teacher provides many code examples alongside the theory and I am learning while building real-life applications.

During the last lecture, I again encountered the useEffect hook which I had already used in the past to fetch some data from an external API. This time though, the hook included a slightly more advanced React feature, the useEffect cleanup function

The page I was building includes a login form that allows the user to fill in email and password fields, validate them, and login into a service after clicking on a button. The goal was to efficiently validate the user input, without code repetitions and, it turned out, useEffect together with the cleanup function was the solution.

Before diving into the actual code, let's have a quick refresh of what the useEffect hook is.

The useEffect hook

Simply put, useEffect is a hook that runs a code in response to something. It is a function that takes two parameters: the "side effect function", and an optional dependency array that controls when the side effect should run. 

This is what the code structure looks like:

useEffect (()=>{
side effect function
} , [dependency array])

When the dependency array is left empty, the useEffect hook only runs after the component renders for the first time. This is because useEffect runs when the values in the dependency array change and, if there are no values to be changed, useEffect is triggered only once. This is a common pattern whenever we want to do something at the beginning of the lifecycle of a component, such as fetching some data.

In our case though, we wanted useEffect to run whenever some data changed (the email and password that need to be validated). Let’s see how this is achieved!

The initial code

Let’s have a look at the initial code:

const Login = (props) => {
const [enteredEmail, setEnteredEmail] = useState("");
const [enteredPassword, setEnteredPassword] = useState("");
const [formIsValid, setFormIsValid] = useState(false);
 
useEffect(() => {
setFormIsValid(
enteredEmail.includes("@") && enteredPassword.trim().length > 6
);
}, [enteredEmail, enteredPassword]);

  const emailChangeHandler = (e) => {
    setEnteredEmail(e.target.value);
  };
 
  const passwordChangeHandler = (e) => {
    setEnteredPassword(e.target.value);
  };
 
  const submitHandler = (event) => {
    event.preventDefault();
    props.onLogin(enteredEmail, enteredPassword);
  };
 
  return (
    <Card className={classes.login}>
      <form onSubmit={submitHandler}>
        <div
          className={classes.control}
        >
          <label htmlFor="email">E-Mail</label>
          <input
            type="email"
            id="email"
            value={enteredEmail}
            onChange={emailChangeHandler}
          />
        </div>
        <div
          className={classes.control}>
          <label htmlFor="password">Password</label>
          <input
            type="password"
            id="password"
            value={enteredPassword}
            onChange={passwordChangeHandler}
          />
        </div>
        <div className={classes.actions}>
          <Button type="submit" disabled={!formIsValid}>
            Login
          </Button>
        </div>
      </form>
    </Card>
  );
};

formIsValid is a boolean value that dictates whether the login button is disabled or not. Only after the inputs are validated, the boolean value becomes true and the button becomes clickable.

We are storing what the user types in the email and password fields inside the enteredEmail and enteredPassword states. Both states are inside the dependency array of useEffect and that means that, whenever React detects a change in one of them, useEffect is run and the inputs are validated (the email needs to contain the @ symbol and the password must be longer than 6 characters).

This first solution is not great as useEffect runs for every single keystroke. A more efficient approach would be to run the validation code only after the user has entered the entire input.

The lecture becomes more intriguing once it is revealed that there is a way to do this...

setTimeout() method

We start by adding a setTimeout() method inside our hook. The idea is to validate the input only after a certain amount of seconds have passed.

The code is the following:

setTimeout(() => {
      console.log("Checking form validity!");
      setFormIsValid(
        enteredEmail.includes("@") && enteredPassword.trim().length > 6
      );
    }, 2000);
 
        }, [enteredEmail, enteredPassword]);

This tweak brings us one step closer to the solution but it is still not what we are looking for. If we check the console, we see that with this code we only delay the input validation by two seconds but then each keystroke is still being checked. Indeed, Whenever we type (either inside the email or password field) the state changes, and a new setTimeout function is fired. Instead, we should find a way to validate our input only after the user has finished typing. Maybe there is a way to “cancel all the previous timeouts” and only keep the one that is triggered by the last keystroke?

Here is where the concept of cleanup function comes in handy.

The cleanup function

The useEffect Hook is built in a way that we can return a function inside it and that is where the cleanup happens. Generally "The cleanup function prevents memory leaks and removes some unnecessary and unwanted behaviors"

useEffect(() => {
        effect
        return () => {
            cleanup
        }
    }, [input])

The cleanup function runs every time before the useEffect side effect function runs and before the component unmounts.

With this in mind the code is changed as follows:

useEffect(() => {
    const identifier = setTimeout(() => {
      console.log("Checking form validity!");
      setFormIsValid(
        enteredEmail.includes("@") && enteredPassword.trim().length > 6
      );
    }, 2000);
 
    return () => {
      console.log("Cleanup runs");
      clearTimeout(identifier);
    };
  }, [enteredEmail, enteredPassword]);

By adding the cleanup function, whenever we type a new character we clear the timeout that has been set for the previous character (with the clearTimeout method) and therefore our input is only validated once the user has finished typing (after two seconds from the last keystroke).

Console view for cleanup

With this final code, if we type 6 characters inside the form (either email or password fields) while checking the console,  we see that the cleanup function runs 6 times but only once the input is validated.

Cool right? It took me a while to wrap my head around the concept of cleanup function but then I finally understood how handy it can be to write better React applications.