Prop spreading for prop drilling is in fact an anti pattern. However, having a known type and being able to spread it to create a new type doesn’t need necessarily to be tied to prop drilling. I think it is an useful feature that Ocaml has to some degree with modules. You can include a module into another module and have all its props and methods, this is some kind similar.
However, I have been using Flow without spread support and with it (for types) and I didn’t noticed any noticeable productivity boost
OCaml has ‘object spread’ already:
type person = < name : string >
type employee = < person; id : int >
You can do this today in ReScript if you want, in OCaml syntax.
I think the only case where props spreading might be a non-smell is when the value being spread is the only thing that you pass to a component, like this:
<Foo {...p} />
Because then it gets compiled straight to:
React.createElement(Foo, p);
But when it’s <Foo {...p} x={x} />
, Object.assign
kicks in and all hell breaks loose
Ocaml is the syntax I that I prefer so I’m glad it’s already possible
Ooh, I didn’t know this is possible in OCaml.
I mean, this is the only thing I’m even wanting/proposing here. I also agree prop spread in actual variables (or react props) is an antipattern.
But I encountered this problem when creating bindings for react-select
version 3. In many of its components it defines the prop API like this: https://github.com/JedWatson/react-select/blob/4fb2fd608f475efac911fe132184fad913762134/packages/react-select/src/components/Option.js#L24
I think the rationale for that is to allow full flexibility for users to override any of the components and still gain access to all possible props, some of which the default implementation might not even use. It can be of course argued whether that is an anti pattern or not, but in practical life the person trying to use this library has following options:
- Copy-paste the same list of common props (> 10) as many times as there are components (> 10) to create exhaustive bindings. (Feels like a bit too much, and it breaks easily too because there is lots of duplication.)
- Only create bindings for features they personally use, which means bindings cannot be meaningfully shared as OSS
- Rewrite the whole lib (tempting, but some of the libs are quite big and complicated, react-select is one of them)
None of those options seem great, so It would be awesome to see the OCaml feature land to Reason/ReScript too. But of course it would need to work for objects too I think.
There are definitely some use cases where row polymorphism is quite efficient at modeling a problem.
Since OCaml 4.06 (and hence available in latest Rescript) you can do this in ocaml syntax:
type person_obj = < name : string >
type person = person_obj Js.t
type employee = < id : int; person_obj > Js.t
Though, Rescript purposely doesn’t ship with the Ocaml object and class system, so a workaround should be found to express the same thing with rescript syntax:
type person = {
"name": string,
}
type employee = {
...person,
"id": int,
}
That’s precisely one of the reasons why include
is discouraged. To some degree it’s a fancy compile-time copy paste of entire modules again and again. Horrible for code size.
To be fair this is more like including a module type inside another module’s signature. This doesn’t add any code size.
Functors do this, but I don’t believe include
does certainly it hasn’t impacted the code size where I’m looking (I have a library module that does
include Js.String2
and adds a few extra methods).
Might wanna keep an eye on your output then. Js.String2
is externals-only; one of the many reasons why we advocate externals over wrappers. So the output happens to be empty and you only pay the compilation cost, which is also very non-trivial because you’ve essentially added another file to the compilation each time you do include
. There are optimizations to short-circuit things but you get the idea.
Btw, what explains the vast compilation performance of Go & OCaml vs Rust & C++ isn’t the engineering expertise (all of them are very talented) but actually the generic monomorphization strategy (aka the former 2 don’t). include
and functors are our own heavyweight “generic” specialization features; thankfully we don’t have more of those concepts. To avoid ending up with a slow and heavy (in more than one way) compilation, use less of them.
Hi, this can not be done elegantly in ppx, it needs some more internal changes, it’s a nice wish though.
As someone pointed out, it is already possible in structural types.
So for example, the types below is supported and recommended
type t0 = < x : int >
type t1 = < t0 : t0, y : float >
let u : t1 Js.t = [%obj { x = 3 ; y = 3.0} ]
We can still make use of structural types, it is such things below not recommended:
let u = object method x = 3 method y = 3.0 end
It’s not recommended due to the fact that we can not compile it efficiently
It’s probably worth mentioning that these kind of types are causing a lot of terrible error messages in case of a type mismatch, and I’d hate to see this feature exposed without proper facilities to fix type errors.
Ah, that explains it. We only use inline
sparingly, though, in our stdlib (I can see in one of the other modules it does add a bunch of var
s re-exporting the API) and some Functor code.
I have assumed tree shaking bundlers can inline these re-exported API links so it won’t have any impact on prod bundle size, but I haven’t checked.
So what’s the pattern/method of achieving something like this when trying to model things that are built upon base types?
type person = {
name: string
}
type employee = {
...person,
company: string
}
How do you model this idiomatically with rescript/reason?
There is no inheritance or “merging of types” in ReScript.
One possible approach would be nesting of records:
type person = {
name: string
}
type employee = {
person: person,
company: string
}
If you need to store values of both types together (e.g. in an array), you could use variants as well:
type person = {
name: string
}
type employeeDetails = {
company: string,
salary: float
}
type employerDetails = {
company: string,
employees: int
}
type role = | Person(person) | Employee(person, employeeDetails) | Employer(person, employerDetails)
This way it’s also easily possible to pattern match over those types.
This would actually be possible with ocaml objects and hence with rescript JS objects but it lacks the syntax for it. Hopefully it will be added because I think there are use cases for this. Maybe create an issue in the syntax repo?
You can do this at this time
type o = {"hi": int}
type oo = {"hi": int, "lo": int}
let u: o = {"hi": 3}
let uu: oo = {"hi": 3, "lo": 2}
In the future, we plan to remove the need for Js.t, so you can use it as first class.
There seems to be a bug in the rescript syntax, I filed here https://github.com/rescript-lang/syntax/issues/154
In ocaml syntax, you can do it like this
type o = < hi : int >
type oo = < o ; lo : int >
let u : o Js.t = [%obj{ hi = 3 }]
let uu : oo Js.t = [%obj { hi = 3; lo = 1}]
So, what is the modern way to implement common react pattern?
(Component, props) => <Component {...props} override="val" />
I’m thinking of
let f = (component, props) => {
React.createElement(
component,
Js.Obj.empty()->Js.Obj.assign(props)->Js.Obj.assign({"override": "val"}),
)
}
let f2 = (component, props) => {
React.createElement(component, props)->React.cloneElement({"override": "val"})
}
I believe the advice is still to use React.cloneElement
. It’s mentioned in the ReScript-React docs.