Module typing with memoized polymorphic React component

Hi all,

I’ve managed to get myself into a corner. I have a large, complicated React component that I’d like to squirrel away in its own module. And, because it’s large and complicated, I would very much like to memoize it, using a custom comparator to check whether the props that I actually care about have changed.

Because the component is complicated, I would very much like to provide a .resi file where I can provide the type, and some documentation. I cannot work out how to convince Rescript that the types are fine; pretty sure it’s something to do with monomorphic types creeping in, even though it should be completely polymorphic. I’ve linked to a tiny example.

How can I work around this weird monomorphisation issue? It works great without memoizing using the custom prop comparer.

Thanks for any help!

https://rescript-lang.org/try?code=LYewJgrgNgpgBAIQgF2SAdgLjgbwFBxwACATjAIYDGyAdJSMAA4YzrIFyzJzDkDWMbAAoAfsgAWAS3QBzbAHJyAGjgiZMZABUpsheTgBeAHxwI6ScgCUhkwCUK1GjFjBW7AL6HcHUg9r0mFjYOLh5+eAM4UQlpOThFFTUNbVi9G1NzK3T8QkIAHgAjFDR0OAwAYShJSj4DHAB9dPUtHRkhGNlLdxMceypaAGdkElihACIAdxAJsa64PIB6ItQMIw53DhCNMIEvPsdXUHKIIYZyhkZyMgAFEhBGAaFeARUhAA8VAE9rYzg3gG0xh0ZGMALqGAyRT6A4Fgyx4DZ4UIAMxAIC8eSQK1KwIMAEY4M0UrIDAApAY0KAgGRwBZrIA

A bit more digging suggested it might not be solvable, oops! I ended up working around this by using a functor. Not quite as clean, but not bad.

https://rescript-lang.org/try?code=LYewJgrgNgpgBAIQgF2SAdgLjgbwFBxyiSxwCyAhgNYzYAUBhcAKtvk08gJ4AO8yjQgF8ANIwCUcALwA+RuyYABAE4wKAY2QA6dSGA8MMdAI6xkRarTh0AfsgAWAS3QBzbMy3IRcGy5jJmJ1d3T2kZOAh0R2RJWTgAJTVNLRhYYCMTOCE8IWlcRmJoeEoaPIYOVnyOQm4+OEzhMUJY8IVCFSTtXX1DY0E4MwtSqWs7ILcWT29ff0DnCY9zOMjolqrqgB4AIxQ0dDgMAGEoR3UqKRwAfTC4PwDxugd58SFWxI1tAGdkZXm6ACIAO4gQH-F5wDYAeh2qAwcg42X6g2Aljy72S6VAhwg3z0hz0PAoqgACsoQDxPnQUTRvHQAB7eLgtfqEOkAbX+T1c-wAutIpCMuByuS5ef1xIxsojCqQEHkkLD0FoSjA6Apavw8s4BEIJYMAGYgEB5DZykVSACMt1m4ykAClPlooCAXHBIXIgA

This is a good start but if you really need a custom comparator, then the more generic way would be to actually pass in the custom comparator when you call the functor, for example.

If you using === then you’re just using JavaScript’s default strict object equality.

You have two let make statements in there, that ReScript thinks are fine. My head cannot follow, though. The first is a middle-of-the-road creation of a react component.

The second then duplicates the definition (to which my head immediately screams: illegal, already defined!) and I’ll take it, deals with the memoization via some react construct.

My Q, why is this ReScript legal ReScript?

My custom comparator is, in reality, much more complex, doing a deep comparison on nested records that are polymorphic. This was just showing the point. But the comparison is definitely better in the functor — noted!

1 Like

The first let make, the “middle of the road” react component, is the “base” definition — all the usual work happens here. The second let make shadows the first: note how the first make is an argument of the memoization function used to define the second. This is not recursive, as I didn’t use a let rec definition.

Basically I’m trying to exploit let shadowing to hide the fact the component is memoized. In most other contexts you’d use threading (->) or regular function calls, but the @react.component PPX cannot work with those except in very limited cases.

1 Like

This is called “shadowing.” It’s a common pattern in functional languages like ReScript. You can see the docs about it here: Let Binding | ReScript Language Manual

1 Like

thx!

common pattern? Not too sure. But that might be the part of me that has renamed too many shadowed variables in too many languages, to not want to see it. And definitely the fact that JS does not allow it (with ‘let’; with ‘var’ it’s ok).

After digging on the react-memoization, I see this shadowing make also returns a react.component. So that makes it useful for sure (proper name, proper type, off we go)

I do note that the generated JS code has the actual creation in different scopes, there is no shadowing there.

A day on which you learn something, is a good day.

In any language with functions, there is no (good) way you can avoid shadowing. E.g.,

const x = 1

// parameter x shadows const x!
function f(x) { return x }

const y = f(2) // y = 2

To ReScript, a series of let-bindings has the same meaning as a series of functions with shadowed parameter names:

let x = 1
let x = 2
Js.log(x)

// Means the same as...

(x =>
(x => Js.log(x))
(2))
(1)
1 Like

Another thought occurred to me (although late at night, so it could be completely mad):

I could just throw an ->Obj.magic on the end of my memo call… I’ll need to think carefully about whether this is actually safe, but it seems (at first blush) like it should be — the type system is struggling because of the intermediate polymorphic object types being threaded through the memo comparison function; because I know the comparison function isn’t going to be doing anything nefarious, it’s not inherently dangerous. However, the compiler doesn’t know that, so it complains!