You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
React 19 has deprecated forwardRef and now allows passing ref as a regular prop:
// React 19 (JavaScript/TypeScript)functionButton({ ref, children }){return<buttonref={ref}>{children}</button>;}
Current State in ReScript 12: PR #7420 removed the compiler error that blocked ref as a prop, but type support is incomplete:
// ReScript 12 - Compiles but has type errors
@react.componentletmake= (~ref: option<JsxDOM.domRef>=?, ~children) => {
<buttonref={?ref}>{children}</button>
// ❌ Type error: option<option<JsxDOM.domRef>> vs option<JsxDOM.domRef>// The ?ref syntax wraps it in option again, creating nested options
}
Note: ReactDOM.domRef is an alias for JsxDOM.domRef (defined in ReactDOM.res:81). They are interchangeable, and both should work once this proposal is implemented. The current limitation affects both types equally because the JSX transform treats ref as a special prop rather than allowing it as a regular prop type.
Update compiler/syntax/src/jsx_v4.ml at two key locations:
Location 1 - ref prop type handling (line 142-164, specifically line 148-159):
(* Current code in make_props_type_params function *)(* TODO: Worth thinking how about "ref_" or "_ref" usages *)(* line 147 *)elseif label ="ref"thenmatch interior_type with|{ptyp_desc = Ptyp_any} -> Some (ref_type_var loc)
|_ ->
(* Strip explicit Js.Nullable.t in case of forwardRef *)if strip_explicit_js_nullable_of_ref then
strip_js_nullable interior_type
elseSome interior_type
(* Modified for React 19 *)elseif label ="ref"thenif config.react_version >=19then(* React 19: treat ref as a normal prop, no special type handling *)match interior_type with|{ptyp_desc = Ptyp_any} -> Some (Typ.var ~loc (safe_type_from_value (Labelled {txt = label; loc =Location.none})))
|_ -> Some interior_type
else(* React 18 and below: original behavior *)match interior_type with|{ptyp_desc = Ptyp_any} -> Some (ref_type_var loc)
|_ ->
if strip_explicit_js_nullable_of_ref then
strip_js_nullable interior_type
elseSome interior_type
(* Current code in recursively_transform_named_args_for_make function *)if txt ="ref"thenlet type_ =match pattern with|{ppat_desc = Ppat_constraint (_, type_)} -> Some type_
|_ -> Nonein(* The ref arguement of forwardRef should be optional *)
( ( Optional {txt ="ref"; loc =Location.none},
None,
pattern,
txt,
pattern.ppat_loc,
type_ )
:: args,
newtypes,
core_type )
(* Modified for React 19 - ref becomes a regular labeled prop *)if txt ="ref"thenif config.react_version >=19then(* React 19: ref is a normal labeled prop, process like other props *)let type_ =match pattern with|{ppat_desc = Ppat_constraint (_, type_)} -> Some type_
|_ -> Nonein
( ( Labelled {txt ="ref"; loc =Location.none},
None, pattern, txt, pattern.ppat_loc, type_ )
:: args,
newtypes, core_type )
else(* React 18: ref is optional in forwardRef *)let type_ =match pattern with|{ppat_desc = Ppat_constraint (_, type_)} -> Some type_
|_ -> Nonein
( ( Optional {txt ="ref"; loc =Location.none},
None, pattern, txt, pattern.ppat_loc, type_ )
:: args,
newtypes, core_type )
3. Improve Error Messages
When using ref in React 18 mode:
Error: `ref` cannot be passed as a normal prop in React 18.
Options:
1. Upgrade to React 19:
npm install react@19 react-dom@19
2. Use a custom ref callback prop:
~setButtonRef: option<ReactDOM.domRef => unit>=?
See: https://rescript-lang.org/docs/react/latest/forwarding-refs
Breaking Changes
None - This is an opt-in feature:
React 18 users: Existing behavior unchanged (ref still blocked)
React 19 users: ref automatically enabled
Existing React.forwardRef code: Continues to work (React 19 still supports it)
Benefits
✅ Full React 19 alignment - Use standard React patterns
✅ Documentation consistency - Official recommendations actually work
ReScript 12.0.0-alpha.13 included PR #7420, which removed this error:
(* Removed in PR #7420 - Line 254-257 of jsx_v4.ml *)|Pexp_fun{arg_label = Labelled{txt = "ref"} | Optional{txt = "ref"}} ->
Jsx_common.raise_error ~loc:expr.pexp_loc
"Ref cannot be passed as a normal prop. Please use `forwardRef` API \ instead."
This was an important first step, but it only addressed the compiler error. The type system still doesn't properly handle ref as a prop, resulting in type mismatches when trying to use it.
What's Still Missing
While PR #7420 removed the error, developers still face:
Type incompatibility: option<JsxDOM.domRef> doesn't match the expected JsxDOM.domRef type
No prop forwarding: Can't simply pass ~ref through to child components
Workaround required: Must still use ref callback pattern instead of standard React 19 pattern
This proposal completes the work started in PR #7420 by adding full type system support.
Supporting Evidence from Codebase
While analyzing the compiler source, I found this TODO comment in jsx_v4.ml (line 147):
(* TODO: Worth thinking how about "ref_" or "_ref" usages *)
This suggests the team has already been considering improvements to ref handling. This proposal offers a clean solution that aligns with React 19's direction rather than introducing workaround patterns like ref_.
Open Questions
Version detection strategy: I've proposed three options above (A: explicit config, B: auto-detect, C: hybrid). My recommendation is Option C (Hybrid) for the best DX balance. What's the team's preference?
Default behavior: Should the default be:
18 (safe, backward compatible) ← my recommendation
"auto" (convenient, but requires package.json parsing)
19 (forward-looking, but breaking for React 18 users)
JSX version coupling: Should this feature be limited to JSX v4, or also support JSX v3?
Migration guide: Would you like documentation showing how to migrate from forwardRef to ref prop?
Error message format: Is the proposed error message helpful enough, or would you prefer different guidance?
Motivation
React 19 has deprecated
forwardRefand now allows passingrefas a regular prop:Current State in ReScript 12: PR #7420 removed the compiler error that blocked
refas a prop, but type support is incomplete:Note:
ReactDOM.domRefis an alias forJsxDOM.domRef(defined in ReactDOM.res:81). They are interchangeable, and both should work once this proposal is implemented. The current limitation affects both types equally because the JSX transform treatsrefas a special prop rather than allowing it as a regular prop type.This creates an incomplete implementation:
refas prop #7420 removed the compilation error (source)refas prop the standard patternCurrent Workaround
We currently need to use custom callback props:
This works but:
Proposal
Allow
refas a regular prop when React 19 is detected:Technical Approach
I've analyzed the actual codebase (latest
mainbranch) and identified the exact modification points needed.1. React Version Configuration
Current
jsx_common.mlconfig structure (line 4-9):Proposed addition:
Configuration Options
I've identified three possible approaches for React version detection:
Option A: Explicit
rescript.jsonConfiguration{ "jsx": { "version": 4, "module": "React", "reactVersion": 19 } }Ext_json_parseOption B: Auto-detect from
package.json"workspace:*""next","canary","latest""file:...","git+https://...""npm:react@19"A regex like
/^[\^~>=<]*(\d+)/handles most cases, but edge cases require fallback logic.Option C: Hybrid Approach
{ "jsx": { "version": 4, "module": "React", "reactVersion": "auto" // or explicit: 18, 19 } }Behavior:
"auto"→ Attempt package.json detection, fallback to 1818/19→ Explicit override (for edge cases or preference)2. Modify JSX v4 Transform
Update
compiler/syntax/src/jsx_v4.mlat two key locations:Location 1 - ref prop type handling (line 142-164, specifically line 148-159):
Location 2 - forwardRef ref handling (line 316-331):
3. Improve Error Messages
When using
refin React 18 mode:Breaking Changes
None - This is an opt-in feature:
refstill blocked)refautomatically enabledReact.forwardRefcode: Continues to work (React 19 still supports it)Benefits
forwardRefremovalExample Use Cases
Focus Management
Scroll Control
Third-party Libraries
Community Interest
@rescript/react0.14.0 already added "Bindings for new React 19 APIs"Prior Work: PR #7420
ReScript 12.0.0-alpha.13 included PR #7420, which removed this error:
This was an important first step, but it only addressed the compiler error. The type system still doesn't properly handle
refas a prop, resulting in type mismatches when trying to use it.What's Still Missing
While PR #7420 removed the error, developers still face:
option<JsxDOM.domRef>doesn't match the expectedJsxDOM.domReftype~refthrough to child componentsThis proposal completes the work started in PR #7420 by adding full type system support.
Supporting Evidence from Codebase
While analyzing the compiler source, I found this TODO comment in
jsx_v4.ml(line 147):(* TODO: Worth thinking how about "ref_" or "_ref" usages *)This suggests the team has already been considering improvements to ref handling. This proposal offers a clean solution that aligns with React 19's direction rather than introducing workaround patterns like
ref_.Open Questions
Version detection strategy: I've proposed three options above (A: explicit config, B: auto-detect, C: hybrid). My recommendation is Option C (Hybrid) for the best DX balance. What's the team's preference?
Default behavior: Should the default be:
18(safe, backward compatible) ← my recommendation"auto"(convenient, but requires package.json parsing)19(forward-looking, but breaking for React 18 users)JSX version coupling: Should this feature be limited to JSX v4, or also support JSX v3?
Migration guide: Would you like documentation showing how to migrate from
forwardReftorefprop?Error message format: Is the proposed error message helpful enough, or would you prefer different guidance?
References
Feedback welcome! Please let me know if this aligns with ReScript React's direction and if you have suggestions for the implementation approach.