Skip to content

Fix the incorrect type annotation for ref in JSX4 #6718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Improve error when using '@deriving(accessors)' on a variant with record arguments. https://github.com/rescript-lang/rescript-compiler/pull/6712
- Stop escaping JSX prop names with hyphens. https://github.com/rescript-lang/rescript-compiler/pull/6705
- Fix trailing undefined for optional parameters not omitted with `@send` and `@new`. https://github.com/rescript-lang/rescript-compiler/pull/6716
- Fix JSX4 adding the incorrect type annotation for the prop `ref` in React.forwardRef component https://github.com/rescript-lang/rescript-compiler/pull/6718

# 11.1.0-rc.7

Expand Down
31 changes: 16 additions & 15 deletions jscomp/syntax/src/jsx_v4.ml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ let safeTypeFromValue valueStr =
if valueStr = "" || (valueStr.[0] [@doesNotRaise]) <> '_' then valueStr
else "T" ^ valueStr

let refTypeVar loc = Typ.var ~loc "ref"

let refType loc =
Typ.constr ~loc
{loc; txt = Ldot (Ldot (Lident "ReactDOM", "Ref"), "currentDomRef")}
[]
{loc; txt = Ldot (Ldot (Lident "Js", "Nullable"), "t")}
[refTypeVar loc]

type 'a children = ListLiteral of 'a | Exact of 'a

Expand Down Expand Up @@ -279,11 +281,11 @@ let makePropsTypeParams ?(stripExplicitOption = false)
(* TODO: Worth thinking how about "ref_" or "_ref" usages *)
else if label = "ref" then
(*
If ref has a type annotation then use it, else `ReactDOM.Ref.currentDomRef.
If ref has a type annotation then use it, else 'ref.
For example, if JSX ppx is used for React Native, type would be different.
*)
match interiorType with
| {ptyp_desc = Ptyp_any} -> Some (refType loc)
| {ptyp_desc = Ptyp_any} -> Some (refTypeVar loc)
| _ ->
(* Strip explicit Js.Nullable.t in case of forwardRef *)
if stripExplicitJsNullableOfRef then stripJsNullable interiorType
Expand Down Expand Up @@ -1077,7 +1079,14 @@ let mapBinding ~config ~emptyLoc ~pstr_loc ~fileName ~recFlag binding =
(* (ref) => expr *)
let expression =
List.fold_left
(fun expr (_, pattern) -> Exp.fun_ Nolabel None pattern expr)
(fun expr (_, pattern) ->
let pattern =
match pattern.ppat_desc with
| Ppat_var {txt} when txt = "ref" ->
Pat.constraint_ pattern (refType Location.none)
| _ -> pattern
in
Exp.fun_ Nolabel None pattern expr)
expression patternsWithNolabel
in
(* ({a, b, _}: props<'a, 'b>) *)
Expand Down Expand Up @@ -1249,7 +1258,6 @@ let transformSignatureItem ~config item =
let pval_type = Jsx_common.extractUncurried pval_type in
check_string_int_attribute_iter.signature_item
check_string_int_attribute_iter item;
let hasForwardRef = ref false in
let coreTypeOfAttr = Jsx_common.coreTypeOfAttrs pval_attributes in
let typVarsOfCoreType =
coreTypeOfAttr
Expand All @@ -1268,9 +1276,7 @@ let transformSignatureItem ~config item =
(Nolabel, {ptyp_desc = Ptyp_constr ({txt = Lident "unit"}, _)}, rest)
->
getPropTypes types rest
| Ptyp_arrow (Nolabel, _type, rest) ->
hasForwardRef := true;
getPropTypes types rest
| Ptyp_arrow (Nolabel, _type, rest) -> getPropTypes types rest
| Ptyp_arrow (name, ({ptyp_attributes = attrs} as type_), returnValue)
when isOptional name || isLabelled name ->
(returnValue, (name, attrs, returnValue.ptyp_loc, type_) :: types)
Expand All @@ -1290,12 +1296,7 @@ let transformSignatureItem ~config item =
in
let propsRecordType =
makePropsRecordTypeSig ~coreTypeOfAttr ~typVarsOfCoreType "props"
psig_loc
((* If there is Nolabel arg, regard the type as ref in forwardRef *)
(if !hasForwardRef then
[(true, "ref", [], Location.none, refType Location.none)]
else [])
@ namedTypeList)
psig_loc namedTypeList
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like I implemented unnecessary handling in the initial version of JSX4. This handling is unnecessary because we typically use the labeled ~ref in the interface of React.forwardRef implementations. Please see the tests I've added. 34d17b7

in
(* can't be an arrow because it will defensively uncurry *)
let newExternalType =
Expand Down
4 changes: 2 additions & 2 deletions jscomp/syntax/tests/ppx/react/expected/forwardRef.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ module V4A = {
ref?: 'ref,
}

let make = ({?className, children, _}: props<_, _, ReactDOM.Ref.currentDomRef>, ref) =>
let make = ({?className, children, _}: props<_, _, 'ref>, ref: Js.Nullable.t<'ref>) =>
ReactDOM.jsxs(
"div",
{
Expand Down Expand Up @@ -239,7 +239,7 @@ module V4AUncurried = {
ref?: 'ref,
}

let make = ({?className, children, _}: props<_, _, ReactDOM.Ref.currentDomRef>, ref) =>
let make = ({?className, children, _}: props<_, _, 'ref>, ref: Js.Nullable.t<'ref>) =>
ReactDOM.jsxs(
"div",
{
Expand Down
127 changes: 127 additions & 0 deletions jscomp/syntax/tests/ppx/react/expected/forwardRef.resi.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
@@jsxConfig({version: 3})

module V3: {
module FancyInput: {
@obj
external makeProps: (
~className: string=?,
~children: React.element,
~ref: ReactDOM.Ref.currentDomRef=?,
~key: string=?,
unit,
) => {
"className": option<string>,
"children": React.element,
"ref": option<ReactDOM.Ref.currentDomRef>,
} = ""
let make: React.componentLike<
{
"className": option<string>,
"children": React.element,
"ref": option<ReactDOM.Ref.currentDomRef>,
},
React.element,
>
}

module ForwardRef: {
@obj
external makeProps: (
~ref: React.ref<Js.Nullable.t<inputRef>>=?,
~key: string=?,
unit,
) => {"ref": option<React.ref<Js.Nullable.t<inputRef>>>} = ""
let make: React.componentLike<
{"ref": option<React.ref<Js.Nullable.t<inputRef>>>},
React.element,
>
}
}

@@jsxConfig({version: 4, mode: "classic"})

module V4C: {
module FancyInput: {
type props<'className, 'children, 'ref> = {
className?: 'className,
children: 'children,
ref?: 'ref,
}

let make: React.componentLike<
props<string, React.element, ReactDOM.Ref.currentDomRef>,
React.element,
>
}

module ForwardRef: {
type props<'ref> = {ref?: 'ref}

let make: React.componentLike<props<React.ref<Js.Nullable.t<inputRef>>>, React.element>
}
}

module V4CUncurried: {
module FancyInput: {
type props<'className, 'children, 'ref> = {
className?: 'className,
children: 'children,
ref?: 'ref,
}

let make: React.componentLike<
props<string, React.element, ReactDOM.Ref.currentDomRef>,
React.element,
>
}

module ForwardRef: {
type props<'ref> = {ref?: 'ref}

let make: React.componentLike<props<React.ref<Js.Nullable.t<inputRef>>>, React.element>
}
}

@@jsxConfig({version: 4, mode: "automatic"})

module V4A: {
module FancyInput: {
type props<'className, 'children, 'ref> = {
className?: 'className,
children: 'children,
ref?: 'ref,
}

let make: React.componentLike<
props<string, React.element, ReactDOM.Ref.currentDomRef>,
React.element,
>
}

module ForwardRef: {
type props<'ref> = {ref?: 'ref}

let make: React.componentLike<props<React.ref<Js.Nullable.t<inputRef>>>, React.element>
}
}

module V4AUncurried: {
module FancyInput: {
type props<'className, 'children, 'ref> = {
className?: 'className,
children: 'children,
ref?: 'ref,
}

let make: React.componentLike<
props<string, React.element, ReactDOM.Ref.currentDomRef>,
React.element,
>
}

module ForwardRef: {
type props<'ref> = {ref?: 'ref}

let make: React.componentLike<props<React.ref<Js.Nullable.t<inputRef>>>, React.element>
}
}
85 changes: 85 additions & 0 deletions jscomp/syntax/tests/ppx/react/forwardRef.resi
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
@@jsxConfig({version: 3})

module V3: {
module FancyInput: {
@react.component
let make: (
~className: string=?,
~children: React.element,
~ref: ReactDOM.Ref.currentDomRef=?,
) => React.element
}

module ForwardRef: {
@react.component
let make: (~ref: React.ref<Js.Nullable.t<inputRef>>=?) => React.element
}
}

@@jsxConfig({version: 4, mode: "classic"})

module V4C: {
module FancyInput: {
@react.component
let make: (
~className: string=?,
~children: React.element,
~ref: ReactDOM.Ref.currentDomRef=?,
) => React.element
}

module ForwardRef: {
@react.component
let make: (~ref: React.ref<Js.Nullable.t<inputRef>>=?) => React.element
}
}

module V4CUncurried: {
module FancyInput: {
@react.component
let make: (
~className: string=?,
~children: React.element,
~ref: ReactDOM.Ref.currentDomRef=?,
) => React.element
}

module ForwardRef: {
@react.component
let make: (~ref: React.ref<Js.Nullable.t<inputRef>>=?) => React.element
}
}

@@jsxConfig({version: 4, mode: "automatic"})

module V4A: {
module FancyInput: {
@react.component
let make: (
~className: string=?,
~children: React.element,
~ref: ReactDOM.Ref.currentDomRef=?,
) => React.element
}

module ForwardRef: {
@react.component
let make: (~ref: React.ref<Js.Nullable.t<inputRef>>=?) => React.element
}
}

module V4AUncurried: {
module FancyInput: {
@react.component
let make: (
~className: string=?,
~children: React.element,
~ref: ReactDOM.Ref.currentDomRef=?,
) => React.element
}

module ForwardRef: {
@react.component
let make: (~ref: React.ref<Js.Nullable.t<inputRef>>=?) => React.element
}
}