Search code examples
reactjsreact-hookstimeoutalert

Timeout alert message causes infinite loop in React page


I am making an app the is supposed to flash a message indicating user creation success or failure. I am using deno's fresh framework which employs "preact". Preact is essentially a minimal version of react so just assume that what I'm doing would work the same in a React page.

My webpage code looks like this:

// import { useState } from "preact/hooks";
import { useState, useEffect } from "preact/hooks";
import { Handlers, PageProps } from "$fresh/server.ts";
import UserDb from "../database.ts";

interface CreatorProps {
  email: string,
  key: string
}

export default function Creator(props: CreatorProps) {
  const [alertPointer, setAlertPointer] = useState(0);
  const [alertInst, setAlertInst] = useState("");

  useEffect(() => {
    function createAlert() {
      if (alertPointer === 0) {
        null;
      } else if (alertPointer === 1) {
        setAlertInst(
          <div class="bg-blue-100 border-t border-b border-blue-500 text-blue-700 px-4 py-3" role="alert">
            <p class="font-bold">User Creation Succeeded</p>
            <p class="text-sm">A new user was added to the database</p>
          </div>
        );
      } else {
        setAlertInst(
          <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
            <strong class="font-bold">User Creation Failed</strong>
            <span class="block sm:inline">Are you sure you entered in a valid email and key?</span>
            <span class="absolute top-0 bottom-0 right-0 px-4 py-3">
              <svg class="fill-current h-6 w-6 text-red-500" role="button" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><title>Close</title><path d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"/></svg>
            </span>
          </div>
        )
      }
    }
    const timeId = setTimeout(() => {
      // After 3 seconds set the show value to false
      setAlertPointer(0);
      setAlertInst("");
    }, 3000)

    function removeItem() {
      console.log("rm item start");
      clearTimeout(timeId)
      console.log("rm item finished");
      console.log(timeId);
      console.log(alertPointer);
      console.log(alertInst);
    }
    createAlert();
    removeItem();
  }, [alertPointer, alertInst]);
  
  async function handleSubmit(event) {
    event.preventDefault();
    const emailInput = event.target.email;
    const ageInput = event.target.key;
    const resp = await createNewUser(emailInput.value, ageInput.value);
    return resp
  };

  async function createNewUser(email, key) {
    const rawPosts = await fetch('http://localhost:8000/api/createUser', {
      "method": "POST",
      "headers": {
        "content-type": "application/json"
      },
      "body": JSON.stringify({
        email: email,
        key: key,
      })
    });
    const respns = JSON.parse(await rawPosts.json());
    if (respns.isUserCreated) {
      setAlertPointer(1);
    } else {
      setAlertPointer(2);
    }
  }

  return (
    <div>
      {alertInst}
      <h1 class="text rounded-lg p-4 my-8"> Search </h1>
      <form method="post" onSubmit={async (e) => handleSubmit(e)}>
        <input class="center rounded-lg p-4 my-8" id="email" name="email" />
        <input class="center rounded-lg p-4 my-8" id="key" name="key" />
        <br />
        <button
          class="px-5 py-2.5 text-sm font-medium bg-blue-600 rounded-md shadow disabled:(bg-gray-800 border border-blue-600 opacity-50 cursor-not-allowed)"
          type="submit">Submit
        </button>
      </form>
      <br />
      {/* <ul>
        {results.map((name) => <li key={name}>{name}</li>)}
      </ul> */}
    </div>
  );
};

When I create a user that succeeds the page looks like this:
enter image description here

But the message never disappears and the web console looks like this:
enter image description here

How do I fix this? How do I make the alert indicating success or failure vanish after 3 seconds?


Solution

  • The issue I see is that once the alertPointer state is updated to a non-zero value it's never set back to 0. The removeItem is called and clears the running timeout that was just instantiated to reset the alertPointer state back to 0. alertPointer equals 1 and state updates are continued to be enqueued.

    useEffect(() => {
      function createAlert() {
        if (alertPointer === 0) {
          null;
        } else if (alertPointer === 1) {
          setAlertInst(....);
        } else {
          setAlertInst(....)
        }
      }
    
      const timeId = setTimeout(() => { // (1) <-- timeout instantiated
        // After 3 seconds set the show value to false
        setAlertPointer(0);
        setAlertInst("");
      }, 3000)
    
      function removeItem() {
        console.log("rm item start");
        clearTimeout(timeId); // (3) <-- cleared
        console.log("rm item finished");
        console.log(timeId);
        console.log(alertPointer);
        console.log(alertInst);
      }
    
      createAlert();
      removeItem(); // (2) <-- timeout cleared
    }, [alertPointer, alertInst]);
    

    You should store the timer id in a React ref (via the useRef hook) and return useEffect cleanup function in the case the component unmounts prior to the timeout expiring. This allows the normal operation of the timeout while the component is mounted.

    Example:

    const timerIdRef = useRef();
    
    // Clear any running timeouts when the component unmounts
    useEffect(() => {
      return () => {
        clearTimeout(timerIdRef.current);
      };
    }, []);
    
    useEffect(() => {
      function createAlert() {
        if (alertPointer === 0) {
          null;
        } else if (alertPointer === 1) {
          setAlertInst(....);
        } else {
          setAlertInst(....)
        }
      }
    
      timerIdRef.current = setTimeout(() => {
        // After 3 seconds set the show value to false
        setAlertPointer(0);
        setAlertInst("");
      }, 3000)
    
      createAlert();
    }, [alertPointer, alertInst]);