reason-react
reason-react copied to clipboard
React.forwardRef is unsafe
Usecase:
module X = {
[@react.component]
let make = React.forwardRef(theRef =>
<div ref=?{theRef->Js.Nullable.toOption->Belt.Option.map(ReactDOMRe.Ref.domRef)} />
);
};
[@react.component]
let make = () => <X ref={theRef => theRef->React.Ref.current->Js.log} />;
This compiles b/c theRef
type is inferred in the callback, but it crashes at runtime since in this case theRef
is of type Js.nullable(Dom.element)
.
I investigated this a bit as part of the work in rroo.
The ref
exposed by forwardRef
is of type 'a
, so it can be matched with anything really. If X
is declared like above, this type checks as well:
[@react.component]
let make = () => <X ref=12345 />;
I believe one fix could be:
- Simplify the types in
forwardRef
, by usingReactDOMRe.domRef
type. This doesn't fix anything but I think it'd help avoid issues in the future if some of these types change. 😄
[@bs.module "react"]
external forwardRef:
([@bs.uncurry] (('props, ReactDOMRe.domRef) => React.element)) =>
React.component('props) =
"";
-
Make sure the JSX ppx doesn't generate a type variable for
ref
and instead refine it by making it of typeReactDOMRe.domRef
. I believe the ppx code doesn't live in this repo anymore and it's part of thejscomp/syntax
in BuckleScript repo. The line to be changed is this:https://github.com/BuckleScript/bucklescript/blob/89efbef61090b41e083387802f74a49879bff344/jscomp/syntax/reactjs_jsx_ppx.cppo.ml#L772
Instead of the
None
at the end of the tuple, it should beSome(domRefDef)
wheredomRefDef
is an ast typecore_type
that representsReactDOMRe.domRef
(example in astexplorer).
If anyone wants to explore how these changes would look like, here's a version of X
above with the changes applied, unppxed version included for clarity:
[@bs.module "react"]
external forwardRef:
([@bs.uncurry] (('props, ReactDOMRe.domRef) => React.element)) =>
React.component('props) =
"";
module X = {
[@react.component]
let make =
forwardRef(theRef => {
<div ref=theRef> "ForwardRef"->React.string </div>;
});
};
// Would compile to:
module X = {
[@bs.obj]
external makeProps:
(~key: string=?, ~ref: ReactDOMRe.domRef=?, unit) => Js.t({.}) =
"";
let make =
forwardRef(
{
let test = (_props: Js.t({.}), theRef) => {
ReactDOMRe.createDOMElementVariadic(
"div",
~props=ReactDOMRe.domProps(~ref=theRef, ()),
[|"ForwardRef"->React.string|],
);
};
test;
},
);
};
Then calling
<X ref={theRef => theRef->React.Ref.current->Js.log} />
would fail with:
Error: This expression should not be a function, the expected type is ReactDOMRe.domRef
Here's the related PR in rroo, in case it helps: https://github.com/jchavarri/rroo/pull/19. The required changes in the ppx are tiny: https://github.com/jchavarri/rroo/pull/19/files#diff-ec2648e0bf16200f53280d61f4d4ee33.
One thing to note is that I had to move forwardRef
from React
to ReactDOM
module to avoid cyclic deps.
Hey folks,
For the sake of cleaning up the repo (and given how old this issue is) I'm going to close this out.
Thank you and sorry about the delay. Please re-open if its still relevant!
Re-opening since this might still be relevant