[SOLVED] React Hooks: how to make a timer that potentially doesn't change state?

I fetch some data once per second from the server. I know ideally WebSockets have to be used, but for starters I’m just sticking to a timer.

Now, I see the structure to the solution is like:

  1. Make state /\ setState pair via useState
  2. Call to timer inside useAff, and use the pair from 1 to change state as needed.

The problem though is that to make sure useAff is called every time, it’d have to depend on state. But if my timer wouldn’t change state just once (e.g. because data it fetched remained unchanged), it would never get called again!

Then, solution it seems is to ignore deps and to simply never return from the timer (by using forever). But this doesn’t work because apparently React Hooks don’t re-render per setState call.

Implementation for the latter:

module Main where

import Prelude

import Control.Monad.Rec.Class (forever)
import Data.Maybe (Maybe(..))
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Aff (Aff, Milliseconds(..), delay, forkAff)
import Effect.Class (liftEffect)
import Effect.Exception (throw)
import React.Basic.DOM as R
import React.Basic.DOM.Client (createRoot, renderRoot)
import React.Basic.Hooks (Component, component, useState')
import React.Basic.Hooks as React
import React.Basic.Hooks.Aff (useAff)
import Web.DOM.NonElementParentNode (getElementById)
import Web.HTML (window)
import Web.HTML.HTMLDocument (toNonElementParentNode)
import Web.HTML.Window (document)

main :: Effect Unit
main = do
  doc <- document =<< window
  root <- getElementById "root" $ toNonElementParentNode doc
  case root of
    Nothing -> throw "Could not find root."
    Just container -> do
      reactRoot <- createRoot container
      app <- myComponent
      renderRoot reactRoot (app {})

myTimer :: Int -> (Int -> Effect Unit) -> Aff Unit
myTimer count setCount = do
  _ <- forkAff $ forever do
    delay $ Milliseconds 1000.0
    liftEffect $ setCount $ count + 1
  pure unit

myComponent :: Component {}
myComponent =
  component "Label" \_ -> React.do
    count /\ setCount <- useState' 0
    useAff unit $ myTimer count setCount
    pure $ R.label {children: [R.text (show count)]}

So how to do that properly?

As a hack for now I combined the idea of “deps change” with the necessity to sometimes not to change the data the timer works with, by providing a “dummy” dep that always changes.

Thinking of it, this hack may probably be made into a new hook of its own.

…

data StateChanger a = StateChanger a (a -> Effect Unit)

myTimer :: StateChanger Boolean -> StateChanger Int -> Aff Unit
myTimer (StateChanger reRender toggleReRender) (StateChanger count setCount) = do
  delay $ Milliseconds 1000.0
  liftEffect do
    toggleReRender $ not reRender -- mandatory state change for timer to work
    setCount $ count + 1
  pure unit

myComponent :: Component {}
myComponent =
  component "Label" \_ -> React.do
    reRender /\ toggleReRender <- useState' true
    count /\ setCount <- useState' 0
    useAff reRender $ myTimer (StateChanger reRender toggleReRender)
                              (StateChanger count setCount)
    pure $ R.label {children: [R.text (show count)]}

A proper solution is welcome though.

Hey there! Nice work.

Here’s a minimally modified version of your first example that works (it uses useState instead of useState’):

myTimer :: ((Int -> Int) -> Effect Unit) -> Aff Unit
myTimer setCount = do
  _ <- forkAff $ forever do
    delay $ Milliseconds 1000.0
    liftEffect $ setCount (_ + 1)
  pure unit

example :: Effect JSX
example = myComponent <@> {}

myComponent :: Component {}
myComponent =
  component "Label" \_ -> React.do
    count /\ setCount <- useState 0
    useAff unit $ myTimer setCount
    pure $ R.label {children: [R.text (show count)]}
1 Like

Ooh, right, I didn’t guess what the first arg to function returned by the original useState was. Both useState’s are meticulously undocumented, so I had to work by guessing there and missed it.

I would’ve never guessed though the side-effect that useState always causes re-render upon state change whereas useState' does not. Thank you!