Skip to content
This repository has been archived by the owner on Jun 15, 2023. It is now read-only.

Latest commit

 

History

History
435 lines (321 loc) · 10.1 KB

JSXV4.md

File metadata and controls

435 lines (321 loc) · 10.1 KB

Introduction

JSX V4, supported in the compiler version introduces a new idiomatic record-based representation of components which is incompatible with V3. Because of this, either the entire project or dependencies need to be compiled in V4 mode, or some compatibility features need to be used to mix V3 and V4 in the same project. The V4 representation is part of the spec, so @react.component is effectively just an abbreviation for code that can be written by hand.

Turn On V4

To build an entire project in V4 mode, including all its dependencies, use the new "jsx" configuration in bsconfig.json instead of the old "reason":

"jsx": { "version": 4 }

Note that JSX V4 requires the rescript compiler 10.1 or higher, and rescript-react version 0.11 or higher. In addition, react version 18.0 is required.

Configuration And Upgrade

Dependency-level config

Dependencies inherit the jsx configuration of the root project. So if the root project uses V4 then the dependencies are built using V4, and the same for V3. To build certain dependencies in V3 compatibility mode, whatever the version used in the root project, use "v3-dependencies": the listed dependencies will be built in V3 mode, and in addition -open ReactV3 is added to the compiler options.

For example, suppose a V3 project uses rescript-react 0.11, which requires compatibility mode if compiled with V3, and that 2 dependencies "rescript-react-native", "rescript-react-navigation" only build with compatibility mode. Then the setting will be:

"jsx": {
  "version": 3,
  "v3-dependencies": ["rescript-react-native", "rescript-react-navigation"]
},
"bsc-flags": ["-open ReactV3"]

Another example is a V4 project that also uses "rescript-react-native", "rescript-react-navigation". Then the setting will be:

"jsx": {
  "version": 4,
  "v3-dependencies": ["rescript-react-native", "rescript-react-navigation"]
}

Note: do not add @rescript/react to the v3-dependencies, or it will cause a cyclic dependencies error.

Classic and Automatic Mode

Classic mode is the default and generates calls to React.createElement just as with V3.

"jsx": {
  "version": 4,
  "mode": "classic"
}

Automatic mode is an experimental mode that generate calls to _jsx functions (similar to TypeScript's react-jsx mode)

"jsx": {
  "version": 4,
  "mode": "automatic"
}

File-level config

The top-level attribute @@jsxConfig is used to update the jsx config for the rest of the file (or until the next config update). Only the values mentioned are updated, the others are left unchanged.

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

module Wrapper = {
  module R1 = {
    @react.component  // V4 and new _jsx transform
    let make = () => body
  }

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

  module R2 = {
    @react.component  // V4 with `React.createElement`
    let make = () => body
  }
}

@@jsxConfig({ version: 3 })

@react.component  // V3
let make = () => body

Migration of V3 components that depend on the internal representation

Some components in existing projects are written in a way that is dependent on the V3 internal representation. Here are a few examples of how to convert them to V4.

makeProps does not exist in V4

Rewrite this:

// V3
module M = {
  @obj external makeProps: (~msg: 'msg, ~key: string=?, unit) => {"msg": 'msg} = "" // No more makeProps

  let make = (~msg) => {
    <div> {React.string(msg)} </div>
  }
}

To this:

// V4
module M = {
  type props<'msg> = {msg: 'msg}
  let make = props => <div> {React.string(props.msg)} </div>
}

React.Context

Rewrite this:

module Context = {
  let context = React.createContext(() => ())

  module Provider = {
    let provider = React.Context.provider(context)

    @react.component
    let make = (~value, ~children) => {
      React.createElement(provider, {"value": value, "children": children}) // Error
    }
  }
}

To this:

module Context = {
  let context = React.createContext(() => ())

  module Provider = {
    let make = React.Context.provider(context)
  }
}

React.forwardRef(Discouraged)

forwardRef is discouraged, but sometimes used in existing V3 code such as this example:

module FancyInput = {
  @react.component
  let make = React.forwardRef((
    ~className=?,
    ~children,
    ref_, // argument
  ) =>
    <div>
      <input
        type_="text"
        ?className
        ref=?{ref_->Js.Nullable.toOption->Belt.Option.map(ReactDOM.Ref.domRef)}
      />
      children
    </div>
  )
}

@react.component
let make = () => {
  let input = React.useRef(Js.Nullable.null)

  <div>
    <FancyInput ref=input> // prop
      <button onClick> {React.string("Click to focus")} </button>
    </FancyInput>
  </div>
}

In this example, there is an inconsistency between ref as prop and ref_ as argument. With JSX V4, ref is only allowed as an argument.

module FancyInput = {
  @react.component
  let make = React.forwardRef((
    ~className=?,
    ~children,
    ref, // only `ref` is allowed
  ) =>
    <div>
      <input
        type_="text"
        ?className
        ref=?{ref->Js.Nullable.toOption->Belt.Option.map(ReactDOM.Ref.domRef)}
      />
      children
    </div>
  )
}

@react.component
let make = () => {
  let input = React.useRef(Js.Nullable.null)

  <div>
    <FancyInput ref=input>
      <button onClick> {React.string("Click to focus")} </button>
    </FancyInput>
  </div>
}

V4 Spec

This is the specification that decribes the two JSX V4 transformations:

  • For component definition @react.component let make = ...
  • For component application <Foo x y />

The transformations are optional in that it is possible to write the resulting code manually instead of using them.

Pre-transformation for component definition

To simplify the description of component definition, a pre-transformation is used to move @react.component to a place where the actual transformations operate.

Normal Case

@react.component
let make = (~x, ~y, ~z) => body

is pre-transformed to

let make = @react.component (~x, ~y, ~z) => body

Forward Ref

@react.component
let make = React.forwardRef((~x, ~y, ref) => body)

is pre-transformed to

let make = React.forwardRef({
  let fn =
    @react.component (~x, ~y) => ref => body
  (props, ref) => fn(props, ref)
})

Transformation for Component Definition

@react.component (~x, ~y=3+x, ~z=?) => body

is transformed to

type props<'x, 'y, 'z> = {x: 'x, y?: 'y, z?: 'z}

({x, ?y, ?z}: props<_, _, _>) => {
  let x = x
  let y = switch y {
  | None => 3 + x
  | Some(y) => y
  }
  let z = z
  body
}

Note: this implicit definition of type props means that there cannot be other type definitions of props in the same scope, or it will be a compiler error about multiple definitions of the type name.

Transformation for Component Application

<Comp x>
// is transformed to
React.createElement(Comp.make, {x: x})

<Comp x y=7 ?z>
// is transformed to
React.createElement(Comp.make, {x, y: 7, ?z})

<Comp x key="7">
// is transformed to
React.createElement(Comp.make, React.addKeyProp(~key="7", {x: x}))

<Comp x key=?Some("7")>
// is transformed to
React.createElement(Comp.make, React.addKeyProp(~key=?Some("7"), {x: x}))

New experimental automatic mode

The V4 ppx supports the new jsx transform of React.js.

The jsx transform only affects component application, but not the definition.

<Comp x>
// is transformed to
React.jsx(Comp.make, {x: x})
<div name="div" />
// is transformed to
ReactDOM.jsx("div", { name: "div" })

The props type of dom elements, e.g. div, is inferred to ReactDOM.domProps.

type domProps = {
  key?: string,
  id?: string,
  ...
}

Interface And External

@react.component (~x: int, ~y: int=?, ~z: int=?) => React.element

// is transformed to

type props<'x, 'y, 'z> = {x: 'x, y?: 'y, z?: 'z}

props<int, int, int> => React.element

Since an external is a function declaration, it follows the same rule.

Component Name

The convention for names is the same one used in V3: the generated function has the name of the enclosing module/file.

Fragments

<> comp1 comp2 comp3 </>

// is transformed to

// v4
ReactDOMRe.createElement(ReasonReact.fragment, [comp1, comp2, comp3])

// v4 @ new jsx transform
React.jsxs(React.jsxFragment, {children: [comp1, comp2, comp3]})

Spread props (new feature)

V4 introduces support for the spread operator for props: {...p}.

module A = {
  @react.component
  let make = (~x, ~y) => body
}

let p: A.props<_> = {x: "x", y: "y"}

<A {...p}>
<A {...p} x="X">

// not allowed
<A x="X" {...p}>
<A {...p} {...p1}>

Shared props type (new feature)

V4 introduces support to control the definition of the props type by passing as argument to @react.component the body of the type definition of props. The main application is sharing a single type definition across several components. Here are a few examples:

type sharedprops<'x, 'y> = {x: 'x, y: 'y, z:string}

module C1 = {
  @react.component(:sharedProps<'a, 'b>)
  let make = (~x, ~y) => React.string(x ++ y ++ z)
}

module C2 = {
  @react.component(:sharedProps<string, 'b>)
  let make = (~x, ~y) => React.string(x ++ y ++ z)
}

module C3 = {
  type myProps = sharedProps<int, int>
  @react.component(:myProps)
  let make = (~x, ~y) => React.int(x + y)
}

The generated code (some details removed) looks like this:

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

type sharedprops<'x, 'y> = {x: 'x, y: 'y, z: string}

module C1 = {
  type props<'a, 'b> = sharedProps<'a, 'b>
  let make = ({x, y, _}: props<_>) => React.string(x ++ y ++ z)
}

module C2 = {
  type props<'b> = sharedProps<string, 'b>
  let make = ({x, y, _}: props<_>) => React.string(x ++ y ++ z)
}

module C3 = {
  type myProps = sharedProps<int, int>
  type props = myProps
  let make = ({x, y, _}: props) => React.int(x + y)
}