Polymorphic variant or option state combined with useEffectN?

Has anybody used the following combination and gotten it to work somehow:

module State = {
  type t = option<(int, int)>
}

@react.component
let make = () => {
  let (state, ..) = React..useState(..)

  React.useEffect1(() => {
     ...
     None
   }, [state->Belt.Option.map(((i1, i2)) => i1)]

I used a tuple of integers as a shorthand for some record structure.

What kind of issue do you have with this? Does the effect fire too often? If i1 really is a primitive value that doesn’t change between rerenders, it shouldn’t matter how you obtain it.

Since posting this, I refactored the relevant code to apply the change in my reducer action handlers instead of using useEffect. When I tried using useEffect, I hit an infinite render loop with the data that I was using. I didn’t troubleshoot it in depth.

I also searched around for web advice on using functions in the dependencies and I didn’t find many useful articles.

I will post more to this thread later on, if I attempt to use useEffect again.

1 Like

I’ve hit a similar issue a few times. Here’s code I found in my project:

  let (href, setHref) = React.useState(() => source->SvgX.fromRequired)

  // Hack to make fx ADT not be considered as changed on each re-render. If
  // not converted to a primitive type (e.g. string), it converts to a new JS
  // array each time: new array, new re-render, even if the data is the same.
  let fxIdentity = switch fx {
  | MaskedHsvAdjust({adjustRed, adjustBlue}) =>
    let params =
      [
        adjustRed.hue,
        adjustRed.saturation,
        adjustRed.value,
        adjustBlue.hue,
        adjustBlue.saturation,
        adjustBlue.value,
      ]->Js.Array2.joinWith("-")
    j`masked-hsv-adjust-$params`
  | /* .. */
  }

  React.useEffect2(() => {
    let p = switch fx {
    | MaskedHsvAdjust({maskSource, adjustRed, adjustBlue}) =>
      GlfxExpo.maskedHsvAdjust(~imageSource=source, ~maskSource, ~adjustRed, ~adjustBlue)
    | /* .. */
    }

    p |> Promise.get(result => setHref(_ => result["uri"]))

    // Effect cleanup
    None
  }, (source, fxIdentity))

  // ...

React on its own uses shallow equality comparison for useEffect dependencies. That is, React uses === to test whether an effect should re-run. This projects poorly to data types that are primitive in ReScript (e.g. variants with params) but non-primitive in JS (objects).

A viable solution is, like shown above, to convert all such values to primitive data types like String before passing to useEffectN. But I found it awkward a bit, and hard to diagnose intuitively.

Maybe, the “right” solution would be writing another ReScript’ish useEffect implementation which uses a deep comparison (pervasives (==)?). This should also eliminate the requirement for useEffect2, 3, 4, …, 24: a single version is enough where you either pass a scalar as the dependency or a tuple of arbitrary arity. But I never got enough time/urge to test this idea in real scenarios.

1 Like

Do you mean this implementation to be a replacement of the usual useEffectN? Feels a bit of an overkill. There’s a reason React has been using shallow comparison all these years: with immutable patterns, new object/array reference means new data most of the time. And ReScript obviously lends itself well to immutability.

Good point about variants with params though. But even those can be stable if a variant comes from, say, useState. No setFoo, no new foo. So (just thinking aloud here) in some cases the solution, instead of serializing unstable values, could be to move stuff to state, even though it’s more imperative and less declarative :thinking:

Yes. I didn’t mean that it should be done in rescript-react though. I feel some high volatility in such solution, so never tried it in battle code. But it might be worthwhile. Maybe not, I don’t know. The main concern is performance. However, doing React optimization I can’t remember a case when useEffect comparison ever appears in the performance profile.

:thinking: It should work fine. However, how intuitive is it?! useMemo should work as well.

Yeah, probably. Excessive diffing feels a bit wasteful, but in most cases, it’s probably not a problem. And yeah, functional patterns could be more readable/maintainable.

I’ve always thought it would be better if useEffect required an additional argument for an equality function, so its signature would look kind of like this: (unit => option<unit => unit>, 'a, ('a, 'a) => bool) => unit. (Maybe not exactly, since you’d still need to enforce that the second argument is a JS array, but that’s the general idea.)

And we could use it like this:

useEffect2(() => {
  doSomething(a, b)
  None
}, (a, b), (MyTypeA.eq, MyTypeB.eq))

This is consistent with how other equality functions work in ReScript, i.e. Belt.Map.String.eq. But implementing this would probably also require changing how the ReactJS hooks work.

1 Like