How to implement an optional setTimeout in a useEffect?

Hello!

I’m having trouble implementing something in ReScript + React that is simple in normal JavaScript + React.

I want to remove a success message 3 seconds after it appears.

Here’s what I’m trying to do:

// JavaScript + React
useEffect(() => {
    if (shouldShowSuccess) {
        setTimeout(() => setShouldShowSuccess(false), 3000)
    }
}, [shouldShowSuccess])

However, I can’t even get this working without the if statement:

// ReScript + React
React.useEffect1(() => {
    Js.Global.setTimeout(() => setShouldShowSuccess(_prev => false), 3000)
    None
}, [shouldShowSuccess])

It throws this error:

  This has type: Js.Global.timeoutId (defined as Js_global.timeoutId)
  Somewhere wanted: unit

I’ve gotten this to work by first setting the timeout and then clearing it if shouldShowSuccess is false. But that feels unnatural:

React.useEffect1(() => {
    let timeout = Js.Global.setTimeout(() => setShouldShowSuccess(_prev => false), 3000)

    if !shouldShowSuccess {
        Js.Global.clearTimeout(timeout)
    }

    None
}, [shouldShowSuccess]);

What is the correct way to implement this behaviour?

Thank you for your time and help! :smile:

Js.Global.setTimeout returns Js.Global.timeoutId but since you aren’t using it you need to ignore the value. Two styles to do that on that line:

let _ = Js.Global.setTimeout(() => setShouldShowSuccess(_prev => false), 3000)
// or
Js.Global.setTimeout(() => setShouldShowSuccess(_prev => false), 3000)->ignore
2 Likes

Ah, awesome! Exactly what I needed. Thank you!

For others, my code now looks like this:

React.useEffect1(() => {
  if shouldShowSuccess {
    Js.Global.setTimeout(() => setShouldShowSuccess(_prev => false), 3000)->ignore
  }
  None
}, [shouldShowSuccess]);

@Chritical Just a suggestion. Looks like you are not handling the unmounted case. You might need a function like this :point_down:t3:

let setClearTimeout = (fn, t) => {
  open Js.Global
  let timeoutId = fn->setTimeout(t)
  () => timeoutId->clearTimeout
}

So now you can simply write like below, to clearout on component unmounted.

React.useEffect1(() => {
  shouldShowSuccess 
    ? setClearTimeout(() => setShouldShowSuccess(_prev => false), 3000)->Some 
    : None
}, [shouldShowSuccess])

This way you are not ignoring the returned timeoutid and carefully using it for unmounted case.

@praveen your Some return value looks weird… you need to run Js.Global.clearTimeout in your cleanup to prevent the callback to be run when the component has been unmounted.

@react.component
let make = () => {
  let (shouldShowSuccess, setShouldShowSuccess) = React.useState(_ => true)
  React.useEffect1(() => {
    if shouldShowSuccess {
      let timeoutId = Js.Global.setTimeout(
        () => setShouldShowSuccess(_prev => false),
        3000,
      )

      Some(
        () => {
          Js.Global.clearTimeout(timeoutId)
        },
      )
    } else {
      None
    }
  }, [shouldShowSuccess])

  <div />
}

1 Like

@ryyppy

I have used this :point_up:t3: function inside useEffect1. This function is actually returning another function that clears the timeout. I pass this returned function directly into Some. So returned function will be invoked on cleanup.

So what I have done is the same thing that you have written.

ah right, … was just looking at the hook code, and I got confused by all the action going on in one collapsed ternary.

2 Likes

Similar to @praveen ended up creating a util in our codebase to create a timer that returns a cleanup function too:

module Timer = {
  @ocaml.doc("
	* Create a timeout that plays nicely with hooks like useEffect.
	* Takes a function to call and int of ms to wait before calling the f func
	* Returns a callback to clear the timeout but will only fire once otherwise
	* results in noop
	*
	* @example
	*
	* React.useEffect0(() => {
		Some(Hooks.Effects.Timer.make(() => Js.Console.log(\"hi\"), 1000))
	})
	")
  let make = (f, ms) => {
    let timer = Js.Global.setTimeout(f, ms)

    () => Js.Global.clearTimeout(timer)
  }
}

Which can be used like:

React.useEffect1(() => {
  if partyStarted {
    Some(Utils.Timer.make(
      () => Js.log("Party has ended. You don't have to go home, but you can't stay here."),
      3000
    ))
  } else {
    None
  }
}, [partyStarted])
3 Likes