Skip to content

Commit 765b113

Browse files
committed
Improve error message for plain functions used as JSX components
1 parent 97045f7 commit 765b113

4 files changed

Lines changed: 73 additions & 6 deletions

File tree

compiler/ml/error_message_utils.ml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ type type_clash_context =
107107
is_constant: string option;
108108
}
109109
| FunctionArgument of {optional: bool; name: string option}
110+
| JsxComponent
110111
| BracedIdent
111112
| Statement of type_clash_statement
112113
| ForLoopCondition
@@ -127,6 +128,7 @@ let context_to_string = function
127128
| Some TryReturn -> "TryReturn"
128129
| Some StringConcat -> "StringConcat"
129130
| Some (FunctionArgument _) -> "FunctionArgument"
131+
| Some JsxComponent -> "JsxComponent"
130132
| Some ComparisonOperator -> "ComparisonOperator"
131133
| Some IfReturn -> "IfReturn"
132134
| Some TernaryReturn -> "TernaryReturn"
@@ -145,6 +147,7 @@ let error_type_text ppf type_clash_context =
145147
| Some ArrayValue -> "This array item has type:"
146148
| Some (SetRecordField _) ->
147149
"You're assigning something to this field that has type:"
150+
| Some JsxComponent -> "This JSX tag resolves to:"
148151
| _ -> "This has type:"
149152
in
150153
fprintf ppf "%s" text
@@ -162,6 +165,7 @@ let error_expected_type_text ppf type_clash_context =
162165
| None -> ());
163166
164167
fprintf ppf " is expecting:"
168+
| Some JsxComponent -> fprintf ppf "But JSX component positions require:"
165169
| Some ComparisonOperator ->
166170
fprintf ppf "But it's being compared to something of type:"
167171
| Some SwitchReturn -> fprintf ppf "But this switch is expected to return:"
@@ -396,6 +400,17 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf
396400
\ - Remove the @{<info>await@} if this is not expected to be a promise\n\
397401
\ - Wrap the expression in @{<info>Promise.resolve@} to convert the \
398402
expression to a promise"
403+
| Some JsxComponent, _ ->
404+
fprintf ppf
405+
"\n\n\
406+
\ JSX tags must resolve to a React component, not a plain function.\n\n\
407+
\ Possible solutions:\n\
408+
\ - If this function takes labeled props, annotate it with \
409+
@{<info>@react.component@}\n\
410+
\ - If this function takes a single props record, annotate it with \
411+
@{<info>@react.componentWithProps@}\n\
412+
\ - If this is already a valid component-like value, wrap it with \
413+
@{<info>React.component(...)@}"
399414
| Some IfReturn, _ ->
400415
fprintf ppf
401416
"\n\n\

compiler/ml/typecore.ml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2525,7 +2525,10 @@ and type_expect_ ?deprecated_context ~context ?in_function ?(recarg = Rejected)
25252525
wrap_trace_gadt_instances env (lower_args env []) ty;
25262526
begin_def ();
25272527
let total_app = not partial in
2528-
let context = type_clash_context_from_function sexp sfunct in
2528+
let context =
2529+
if transformed_jsx then Some JsxComponent
2530+
else type_clash_context_from_function sexp sfunct
2531+
in
25292532
let args, ty_res, fully_applied =
25302533
match translate_unified_ops env funct sargs with
25312534
| Some (targs, result_type) -> (targs, result_type, true)
@@ -3925,15 +3928,19 @@ and type_application ~context total_app env funct (sargs : sargs) :
39253928
if (not optional) && is_optional l' then
39263929
Location.prerr_warning sarg0.pexp_loc
39273930
(Warnings.Nonoptional_label (Printtyp.string_of_label l));
3931+
let argument_context =
3932+
match (context, args) with
3933+
| Some JsxComponent, [] -> Some JsxComponent
3934+
| Some JsxComponent, _ ->
3935+
type_clash_context_for_function_argument ~label:l' None sarg0
3936+
| _ ->
3937+
type_clash_context_for_function_argument ~label:l' context sarg0
3938+
in
39283939
( sargs,
39293940
omitted,
39303941
Some
39313942
(if (not optional) || is_optional l' then fun () ->
3932-
type_argument
3933-
~context:
3934-
(type_clash_context_for_function_argument ~label:l' context
3935-
sarg0)
3936-
env sarg0 ty ty0
3943+
type_argument ~context:argument_context env sarg0 ty ty0
39373944
else fun () ->
39383945
option_some
39393946
(type_argument
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/jsx_plain_function_component.res:26:10-16
4+
5+
24 │ }
6+
25 │
7+
26 │ let _ = <Wrapper value="hello" />
8+
27 │
9+
10+
This JSX tag resolves to: Wrapper.props => Jsx.element
11+
But JSX component positions require:
12+
React.component<'a> (defined as Jsx.component<'a>)
13+
14+
JSX tags must resolve to a React component, not a plain function.
15+
16+
Possible solutions:
17+
- If this function takes labeled props, annotate it with @react.component
18+
- If this function takes a single props record, annotate it with @react.componentWithProps
19+
- If this is already a valid component-like value, wrap it with React.component(...)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module React = {
2+
type element = Jsx.element
3+
type componentLike<'props, 'return> = Jsx.componentLike<'props, 'return>
4+
type component<'props> = Jsx.component<'props>
5+
6+
external component: componentLike<'props, element> => component<'props> = "%component_identity"
7+
external string: string => element = "%identity"
8+
@module("react/jsx-runtime")
9+
external jsx: (component<'props>, 'props) => element = "jsx"
10+
11+
@module("react/jsx-runtime")
12+
external jsxs: (component<'props>, 'props) => element = "jsxs"
13+
}
14+
15+
module ReactDOM = {
16+
external someElement: React.element => option<React.element> = "%identity"
17+
@module("react/jsx-runtime")
18+
external jsx: (string, JsxDOM.domProps) => Jsx.element = "jsx"
19+
}
20+
21+
module Wrapper = {
22+
type props = {value: string}
23+
let make = (props: props) => <div> {React.string(props.value)} </div>
24+
}
25+
26+
let _ = <Wrapper value="hello" />

0 commit comments

Comments
 (0)