Skip to content

Commit f2fd5e1

Browse files
authored
Add jsx ppx for attribute @react.componentWithProps (#7203)
* add ppx for attribute @react.componentWithProps * clean up the attribute after transformation * use fn_name * add arity=1 * build error using @react.componentWithProps with React.forwardRef * change log
1 parent df61392 commit f2fd5e1

File tree

7 files changed

+374
-4
lines changed

7 files changed

+374
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# 12.0.0-alpha.6 (Unreleased)
1414
- Fix exponential notation syntax. https://github.com/rescript-lang/rescript/pull/7174
1515
- Add `Option.all` & `Result.all` helpers. https://github.com/rescript-lang/rescript/pull/7181
16+
- Add `@react.componentWithProps` that explicitly handles the props with shared props: https://github.com/rescript-lang/rescript/pull/7203
1617

1718
#### :bug: Bug fix
1819
- Fix bug where a ref assignment is moved ouside a conditional. https://github.com/rescript-lang/rescript/pull/7176

compiler/syntax/src/jsx_common.ml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ let has_attr (loc, _) =
1515
| "react.component" | "jsx.component" -> true
1616
| _ -> false
1717

18+
let has_attr_with_props (loc, _) =
19+
match loc.txt with
20+
| "react.componentWithProps" | "jsx.componentWithProps" -> true
21+
| _ -> false
22+
1823
(* Iterate over the attributes and try to find the [@react.component] attribute *)
19-
let has_attr_on_binding {pvb_attributes} =
20-
List.find_opt has_attr pvb_attributes <> None
24+
let has_attr_on_binding pred {pvb_attributes} =
25+
List.find_opt pred pvb_attributes <> None
2126

2227
let core_type_of_attrs attributes =
2328
List.find_map

compiler/syntax/src/jsx_v4.ml

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ let merlin_focus = ({loc = Location.none; txt = "merlin.focus"}, PStr [])
124124
(* Helper method to filter out any attribute that isn't [@react.component] *)
125125
let other_attrs_pure (loc, _) =
126126
match loc.txt with
127-
| "react.component" | "jsx.component" -> false
127+
| "react.component" | "jsx.component" | "react.componentWithProps"
128+
| "jsx.componentWithProps" ->
129+
false
128130
| _ -> true
129131

130132
(* Finds the name of the variable the binding is assigned to, otherwise raises Invalid_argument *)
@@ -941,7 +943,7 @@ let vb_match_expr named_arg_list expr =
941943
aux (List.rev named_arg_list)
942944

943945
let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
944-
if Jsx_common.has_attr_on_binding binding then (
946+
if Jsx_common.has_attr_on_binding Jsx_common.has_attr binding then (
945947
check_multiple_components ~config ~loc:pstr_loc;
946948
let binding = Jsx_common.remove_arity binding in
947949
let core_type_of_attr =
@@ -1189,6 +1191,112 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding =
11891191
Some (binding_wrapper full_expression) )
11901192
in
11911193
(Some props_record_type, binding, new_binding))
1194+
else if Jsx_common.has_attr_on_binding Jsx_common.has_attr_with_props binding
1195+
then
1196+
let modified_binding = Jsx_common.remove_arity binding in
1197+
let modified_binding =
1198+
{
1199+
modified_binding with
1200+
pvb_attributes =
1201+
modified_binding.pvb_attributes |> List.filter other_attrs_pure;
1202+
}
1203+
in
1204+
let fn_name = get_fn_name modified_binding.pvb_pat in
1205+
let internal_fn_name = fn_name ^ "$Internal" in
1206+
let full_module_name =
1207+
make_module_name file_name config.nested_modules fn_name
1208+
in
1209+
1210+
let is_async =
1211+
Ext_list.find_first modified_binding.pvb_expr.pexp_attributes
1212+
Ast_async.is_async
1213+
|> Option.is_some
1214+
in
1215+
1216+
let make_new_binding ~loc ~full_module_name binding =
1217+
let props_pattern =
1218+
match binding.pvb_expr with
1219+
| {pexp_desc = Pexp_apply (wrapper_expr, [(Nolabel, func_expr)])}
1220+
when is_forward_ref wrapper_expr ->
1221+
(* Case when using React.forwardRef *)
1222+
let rec check_invalid_forward_ref expr =
1223+
match expr.pexp_desc with
1224+
| Pexp_fun ((Labelled _ | Optional _), _, _, _, _) ->
1225+
Location.raise_errorf ~loc:expr.pexp_loc
1226+
"Components using React.forwardRef cannot use \
1227+
@react.componentWithProps. Please use @react.component \
1228+
instead."
1229+
| Pexp_fun (Nolabel, _, _, body, _) ->
1230+
check_invalid_forward_ref body
1231+
| _ -> ()
1232+
in
1233+
check_invalid_forward_ref func_expr;
1234+
Pat.var {txt = "props"; loc}
1235+
| {
1236+
pexp_desc =
1237+
Pexp_fun (_, _, {ppat_desc = Ppat_constraint (_, typ)}, _, _);
1238+
} -> (
1239+
match typ with
1240+
| {ptyp_desc = Ptyp_constr ({txt = Lident "props"}, args)} ->
1241+
(* props<_> *)
1242+
if List.length args > 0 then
1243+
Pat.constraint_
1244+
(Pat.var {txt = "props"; loc})
1245+
(Typ.constr {txt = Lident "props"; loc} [Typ.any ()])
1246+
(* props *)
1247+
else
1248+
Pat.constraint_
1249+
(Pat.var {txt = "props"; loc})
1250+
(Typ.constr {txt = Lident "props"; loc} [])
1251+
| _ -> Pat.var {txt = "props"; loc})
1252+
| _ -> Pat.var {txt = "props"; loc}
1253+
in
1254+
1255+
let wrapper_expr =
1256+
Exp.fun_ ~arity:None Nolabel None props_pattern
1257+
(Jsx_common.async_component ~async:is_async
1258+
(Exp.apply
1259+
(Exp.ident
1260+
{
1261+
txt =
1262+
Lident
1263+
(match rec_flag with
1264+
| Recursive -> internal_fn_name
1265+
| Nonrecursive -> fn_name);
1266+
loc;
1267+
})
1268+
[(Nolabel, Exp.ident {txt = Lident "props"; loc})]))
1269+
in
1270+
1271+
let wrapper_expr =
1272+
Ast_uncurried.uncurried_fun ~loc:wrapper_expr.pexp_loc ~arity:1
1273+
wrapper_expr
1274+
in
1275+
1276+
let internal_expression =
1277+
Exp.let_ Nonrecursive
1278+
[Vb.mk (Pat.var {txt = full_module_name; loc}) wrapper_expr]
1279+
(Exp.ident {txt = Lident full_module_name; loc})
1280+
in
1281+
1282+
Vb.mk ~attrs:modified_binding.pvb_attributes
1283+
(Pat.var {txt = fn_name; loc})
1284+
internal_expression
1285+
in
1286+
1287+
let new_binding =
1288+
match rec_flag with
1289+
| Recursive -> None
1290+
| Nonrecursive ->
1291+
Some
1292+
(make_new_binding ~loc:empty_loc ~full_module_name modified_binding)
1293+
in
1294+
( None,
1295+
{
1296+
binding with
1297+
pvb_attributes = binding.pvb_attributes |> List.filter other_attrs_pure;
1298+
},
1299+
new_binding )
11921300
else (None, binding, None)
11931301

11941302
let transform_structure_item ~config item =
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/react_component_with_props.res:4:5-13:10
4+
5+
2 │ @react.componentWithProps
6+
3 │ let make = React.forwardRef((
7+
4 │ ~className=?,
8+
5 │  ~children,
9+
. │ ...
10+
12 │  children
11+
13 │  </div>
12+
14 │ )
13+
15 │ }
14+
15+
Components using React.forwardRef cannot use @react.componentWithProps. Please use @react.component instead.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module V4C7 = {
2+
@react.componentWithProps
3+
let make = React.forwardRef((
4+
~className=?,
5+
~children,
6+
ref: Js.Nullable.t<ReactRef.currentDomRef>,
7+
) =>
8+
<div>
9+
<input
10+
type_="text" ?className ref=?{Js.Nullable.toOption(ref)->Belt.Option.map(React.Ref.domRef)}
11+
/>
12+
children
13+
</div>
14+
)
15+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
let f = a => Js.Promise.resolve(a + a)
2+
3+
@@jsxConfig({version: 4, mode: "classic"})
4+
5+
module V4C1 = {
6+
type props = sharedProps
7+
let make = props => React.string(props.x ++ props.y)
8+
let make = {
9+
let \"SharedPropsWithProps$V4C1" = props => make(props)
10+
\"SharedPropsWithProps$V4C1"
11+
}
12+
}
13+
14+
module V4C2 = {
15+
type props = sharedProps
16+
let make = (props: props) => React.string(props.x ++ props.y)
17+
let make = {
18+
let \"SharedPropsWithProps$V4C2" = (props: props) => make(props)
19+
\"SharedPropsWithProps$V4C2"
20+
}
21+
}
22+
23+
module V4C3 = {
24+
type props<'a> = sharedProps<'a>
25+
let make = ({x, y}: props<_>) => React.string(x ++ y)
26+
let make = {
27+
let \"SharedPropsWithProps$V4C3" = (props: props<_>) => make(props)
28+
\"SharedPropsWithProps$V4C3"
29+
}
30+
}
31+
32+
module V4C4 = {
33+
type props<'a> = sharedProps<string, 'a>
34+
let make = ({x, y}: props<_>) => React.string(x ++ y)
35+
let make = {
36+
let \"SharedPropsWithProps$V4C4" = (props: props<_>) => make(props)
37+
\"SharedPropsWithProps$V4C4"
38+
}
39+
}
40+
41+
module V4C5 = {
42+
type props<'a> = {a: 'a}
43+
let make = async ({a}: props<_>) => {
44+
let a = await f(a)
45+
ReactDOM.createDOMElementVariadic("div", [{React.int(a)}])
46+
}
47+
let make = {
48+
let \"SharedPropsWithProps$V4C5" = (props: props<_>) =>
49+
JsxPPXReactSupport.asyncComponent(make(props))
50+
\"SharedPropsWithProps$V4C5"
51+
}
52+
}
53+
54+
module V4C6 = {
55+
type props<'status> = {status: 'status}
56+
let make = async ({status}: props<_>) => {
57+
switch status {
58+
| #on => React.string("on")
59+
| #off => React.string("off")
60+
}
61+
}
62+
let make = {
63+
let \"SharedPropsWithProps$V4C6" = (props: props<_>) =>
64+
JsxPPXReactSupport.asyncComponent(make(props))
65+
\"SharedPropsWithProps$V4C6"
66+
}
67+
}
68+
69+
@@jsxConfig({version: 4, mode: "automatic"})
70+
71+
module V4A1 = {
72+
type props = sharedProps
73+
let make = props => React.string(props.x ++ props.y)
74+
let make = {
75+
let \"SharedPropsWithProps$V4A1" = props => make(props)
76+
\"SharedPropsWithProps$V4A1"
77+
}
78+
}
79+
80+
module V4A2 = {
81+
type props = sharedProps
82+
let make = (props: props) => React.string(props.x ++ props.y)
83+
let make = {
84+
let \"SharedPropsWithProps$V4A2" = (props: props) => make(props)
85+
\"SharedPropsWithProps$V4A2"
86+
}
87+
}
88+
89+
module V4A3 = {
90+
type props<'a> = sharedProps<'a>
91+
let make = ({x, y}: props<_>) => React.string(x ++ y)
92+
let make = {
93+
let \"SharedPropsWithProps$V4A3" = (props: props<_>) => make(props)
94+
\"SharedPropsWithProps$V4A3"
95+
}
96+
}
97+
98+
module V4A4 = {
99+
type props<'a> = sharedProps<string, 'a>
100+
let make = ({x, y}: props<_>) => React.string(x ++ y)
101+
let make = {
102+
let \"SharedPropsWithProps$V4A4" = (props: props<_>) => make(props)
103+
\"SharedPropsWithProps$V4A4"
104+
}
105+
}
106+
107+
module V4A5 = {
108+
type props<'a> = {a: 'a}
109+
let make = async ({a}: props<_>) => {
110+
let a = await f(a)
111+
ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.int(a)})})
112+
}
113+
let make = {
114+
let \"SharedPropsWithProps$V4A5" = (props: props<_>) =>
115+
JsxPPXReactSupport.asyncComponent(make(props))
116+
\"SharedPropsWithProps$V4A5"
117+
}
118+
}
119+
120+
module V4A6 = {
121+
type props<'status> = {status: 'status}
122+
let make = async ({status}: props<_>) => {
123+
switch status {
124+
| #on => React.string("on")
125+
| #off => React.string("off")
126+
}
127+
}
128+
let make = {
129+
let \"SharedPropsWithProps$V4A6" = (props: props<_>) =>
130+
JsxPPXReactSupport.asyncComponent(make(props))
131+
\"SharedPropsWithProps$V4A6"
132+
}
133+
}

0 commit comments

Comments
 (0)