reasonml / reason-react

Reason bindings for ReactJS
https://reasonml.github.io/reason-react/
MIT License
3.25k stars 347 forks source link

POC of giving a type-error for non keyed elements when are under React.array #812

Closed davesnx closed 1 month ago

davesnx commented 10 months ago

Introduced an idea where there's a difference between elements/components with keys. Useful for the type-checker to raise an issue where keys aren't passed, but React.array needs them.

The implementation of the types goes as follow:

type element('a);

type keyed; /* Flag to mark element('a) to be keyed */
type not_keyed; /* Flag to mark element('a) to be not keyed */

type element_without_key = element(not_keyed);
type element_with_key = element(keyed);

So all usages of element_without_key and element_with_key can be explicit on the bindings.

@johnhaley81


Pros of this PR

let root = ReactDOM.Client.createRoot(container);
let array = [|1, 2, 3|]->Array.map(item => {<div> item->React.int </div>});
ReactDOM.Client.render(root, <div> array->React.array </div>);

Example of the type error:

This expression has type array(React.element(React.not_keyed))
but an expression was expected of type array(React.element_with_key)
Type React.element(React.not_keyed) is not compatible with type
  React.element_with_key = React.element(React.keyed)
Type React.not_keyed is not compatible with type React.keyed

Cons

anmonteiro commented 10 months ago

I need to look at this thoroughly, but one thing that obviously becomes harder is annotating since there's the new phantom type variable for type element.

johnhaley81 commented 10 months ago

cc @mlms13 @maxkorp @andywhite37 @endlo would love your input on this if you can.

jchavarri commented 10 months ago

I started testing the approach in #813 (which is in the end very similar to this 😄 ) and noticed a case in reshowcase that breaks.

The code breaks here with this error:

File "src/ReshowcaseUi.re", lines 327-329, characters 21-15:
327 | .....................{
328 |                 React.null;
329 |               }
Error: This expression has type React.element
       but an expression was expected of type React.elementKeyed

The code does something like this:

  Array.map(((entityName, entity)) => {
    let searchMatchingTerms =
      HighlightTerms.getMatchingTerms(~searchString, ~entityName);

    let isEntityNameMatchSearch =
      searchString == "" || searchMatchingTerms->Belt.Array.size > 0;

    switch (entity) {
    | Demo(_) =>
      if (isEntityNameMatchSearch || parentCategoryMatchedSearch) {
        <Link
          key=entityName
          ...
        />;
      } else {
        React.null;
      }
  ...

I think the error makes sense. I am not sure what the fix should be. React supports null elements intermingled with keyed ones, and it won't show any warnings. Maybe there could be a new element nullKeyed? Which is essentially the null element but with type keyed?

davesnx commented 10 months ago

I believe this is a bug in our code, right? An array of elements where some of them are null seems a bad JSX construction. I have coded myself worst atrocities and later filtering out React.nulls.

jchavarri commented 10 months ago

True. Splitting the processing in 2 steps:

solves the issue. See https://github.com/ahrefs/reshowcase/commit/68718ce5e7addd5e97dc5d35af56ef01c4ced3e9 for the fix (it includes another fix for unneeded key attrs at the bottom).

jchavarri commented 10 months ago

Another interesting case I found is React.Children.mapWithIndex and similar functions, e.g.

[@react.component]
let make = (~children) =>
  <ol>
    {children->React.Children.mapWithIndex((element, index) =>
       <li key={string_of_int(index)}> element </li>
     )}
  </ol>;

I fixed it in https://github.com/reasonml/reason-react/commit/29fc9be70078036f476e63b0993a81c12ec923d3.


I spent some time applying the typed keys to ahrefs monorepo, I am far from finished but I found already 5 bugs of elements that were missing keys. There were also 10+ places where keys were added unnecessarily.

I think the largest pain points are:

In general, my impression is that this improvement could be very useful, and once folks get used to how the types work, the advantages would overcome the downsides. @davesnx @johnhaley81 thanks for proposing it! ❤️

jchavarri commented 10 months ago

Ah, another thing I forgot is that React.Children is quite problematic. I was reading about it on the React official docs and they suggest again using anything in that module, so maybe it could be deprecated eventually?

https://react.dev/reference/react/Children

image
davesnx commented 10 months ago

There were also 10+ places where keys were added unnecessarily

That's very suspicious, people rarely add keys without a good reason.