Conversation
|
@codex review |
|
Codex Review: Didn't find any major issues. 🚀 ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
rescript
@rescript/darwin-arm64
@rescript/darwin-x64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
4d31e9d to
765b113
Compare
mununki
left a comment
There was a problem hiding this comment.
Adding a component-specific identity primitive to allow generalization for the props type of component is a nice improvement. Great idea!
| type componentLike<'props, 'return> = 'props => 'return | ||
| type component<'props> = componentLike<'props, element> | ||
|
|
||
| /* Components consume props. If one component can accept broader props, it can | ||
| safely stand in for a component that only needs narrower props, just like a | ||
| function argument type. That makes the props parameter contravariant. */ | ||
| type component<-'props> | ||
|
|
||
| /* this function exists to prepare for making `component` abstract */ | ||
| external component: componentLike<'props, element> => component<'props> = "%identity" | ||
| external component: componentLike<'props, element> => component<'props> = "%component_identity" |
There was a problem hiding this comment.
I’m in favor of keeping the componentLike type for now to reduce the breaking surface. That said, for a cleaner API long-term, I wonder if we should eventually move away from componentLike and define component directly as if possible:
external component: ('props => element) => component<'props> = "%component_identity"What do you think?
|
What was the issue when this was tried in the past? |
Summary
This PR makes
Jsx.componentan abstract type instead of an alias for a function type.Previously, components were structurally just functions:
That meant any function with the right shape could be used as a React component. This PR changes the public component type to be abstract:
and introduces a new explicit zero-cost component coercion primitive:
The JSX PPX now emits
React.component(...)around generated component wrappers, so component definitions still compile to the same JavaScript while getting the abstract component type in ReScript.This PR also improves the type error for JSX tags that resolve to plain functions. Instead of reporting a generic function-argument mismatch, the compiler now points at the JSX tag and explains that JSX component positions require a
Jsx.component, with suggestions for@react.component,@react.componentWithProps, or an explicitReact.component(...)coercion.Why
React components are not always plain functions.
React.memo,React.forwardRef,React.lazy, and framework-specific wrappers can all produce component values that React accepts, but that should not be called directly as normal functions.With
component<'props>defined as'props => element, ReScript encouraged that wrong mental model: any component could also be treated as a function. Code like this typechecked even though it is not a valid operation for every React component:That can fail at runtime when
componentis not a function. For example, a memoized or lazy component may be represented as an object that React knows how to render, but JavaScript will throw when user code tries to call it directly.Making
componentabstract gives us a single component type that describes values React can render, without pretending those values are structurally just functions. Users can still build components through JSX and the React APIs, but plain functions no longer become components just because their argument and return types happen to match.The props parameter is contravariant because components consume props: a component that accepts broader props can safely be used where a component accepting narrower props is expected.
Example
A component definition still looks the same:
The PPX now conceptually lowers it to:
The generated JavaScript remains a plain function.
%component_identityis new in this PR, but it lowers to the samePidentityLambda primitive as%identity, so it has no runtime cost.Breaking Change
This deliberately tightens the type-system surface around React components.
React.component<'props>is no longer structurally the same type as'props => React.element, so code that relied on that structural equivalence will need to be updated.Code that treats a component as a normal function will stop typechecking:
That code should be rewritten to render through React instead:
Code that passes a plain function to a React API that expects a component also needs an explicit zero-cost coercion:
The same applies when using JSX syntax with a plain props-record function that has not been annotated as a component:
This now reports a JSX-specific error on
A, because the tag resolves to a plain function rather than aJsx.component. For a props-record component like this, the intended fix is@react.componentWithProps. For labeled props, use@react.component.Recursive component bodies have the same requirement. The recursive binding remains the callable implementation function inside its own body, so component-position uses still need an explicit coercion:
The JSX PPX inserts
React.component(...)at the component definition site, around the wrapper function it generates for@react.componentand@react.componentWithProps. It does not rewrite user-authored call sites such asReact.createElement(make, props),React.jsx(make, props),React.jsxs(make, props), or custom runtime calls inside arbitrary function bodies. If user code owns the component-position call, user code owns theReact.component(...)coercion.Custom JSX runtimes that alias
Jsx.componentmust also expose the coercion expected by the PPX:The GenType React fixture patches its npm
@rescript/react@0.14.0dependency to expose the same coercion, because that package version predates%component_identity.For ReScript 13, we will need to publish a matching new
@rescript/reactrelease that definesReact.componentwith%component_identity. Older@rescript/reactversions still define it with%identity, so projects upgrading to ReScript 13 need the corresponding React bindings update as well.New Component Coercion Primitive
This PR adds a new compiler primitive,
%component_identity, specifically for component coercions. It compiles like%identityby lowering toPidentity, but the typechecker gives it one extra rule: fully applied%component_identitycalls are non-expansive when their arguments are non-expansive.This preserves generalization for cases like:
The PPX emits a
React.component(...)wrapper formake. Without the%component_identitynon-expansiveness rule, that wrapper would prevent the inferred polymorphic variant props from generalizing, and the component binding would fail to compile with non-generalized weak type variables.This is intentionally not a general change to
%identity. Plain%identityexternals keep their previous value-restriction behavior, so unrelated casts such asObj.magicor other unsafe identity-shaped externals do not start generalizing differently.%component_identityexists to make the React component coercion explicit enough for the typechecker to recognize without changing the meaning of every existing%identityexternal.Tests
Added and updated coverage for:
React.component%component_identitygeneralization%identityapplications do not receive the new generalization behaviorReact.component(...)coercions for recursive component-position callsReact.createElement@rescript/react@0.14.0coverage in the GenType React fixture