Skip to content

React 19 APIs #133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 4, 2025
3 changes: 0 additions & 3 deletions src/React.bs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 39 additions & 3 deletions src/React.res
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type element = Jsx.element
external float: float => element = "%identity"
external int: int => element = "%identity"
external string: string => element = "%identity"
external promise: promise<element> => element = "%identity"

external array: array<element> => element = "%identity"

Expand Down Expand Up @@ -250,6 +251,9 @@ external useCallback7: ('callback, ('a, 'b, 'c, 'd, 'e, 'f, 'g)) => 'callback =
@module("react")
external useContext: Context.t<'any> => 'any = "useContext"

@module("react")
external usePromise: promise<'a> => 'a = "use"

@module("react") external useRef: 'value => ref<'value> = "useRef"

@module("react")
Expand Down Expand Up @@ -309,10 +313,9 @@ external useImperativeHandle7: (

@module("react") external useId: unit => string = "useId"

@module("react") external useDeferredValue: 'value => 'value = "useDeferredValue"

/** `useDeferredValue` is a React Hook that lets you defer updating a part of the UI. */
@module("react")
external useTransition: unit => (bool, (unit => unit) => unit) = "useTransition"
external useDeferredValue: ('value, ~initialValue: 'value=?) => 'value = "useDeferredValue"

@module("react")
external useInsertionEffectOnEveryRender: (unit => option<unit => unit>) => unit =
Expand Down Expand Up @@ -405,3 +408,36 @@ external setDisplayName: (component<'props>, string) => unit = "displayName"

@get @return(nullable)
external displayName: component<'props> => option<string> = "displayName"

// Actions

type transitionFunction = unit => promise<unit>

type transitionStartFunction = transitionFunction => unit

/** `useTransition` is a React Hook that lets you render a part of the UI in the background. */
@module("react")
external useTransition: unit => (bool, transitionStartFunction) = "useTransition"

type action<'state, 'payload> = ('state, 'payload) => promise<'state>

type formAction<'formData> = 'formData => promise<unit>

/** `useActionState` is a Hook that allows you to update state based on the result of a form action. */
@module("react")
external useActionState: (
action<'state, 'payload>,
'state,
~permalink: string=?,
) => ('state, formAction<'payload>, bool) = "useActionState"

/** `useOptimistic` is a React Hook that lets you optimistically update the UI. */
@module("react")
external useOptimistic: (
'state,
~updateFn: ('state, 'action) => 'state=?,
) => ('state, 'action => unit) = "useOptimistic"

/** `act` is a test helper to apply pending React updates before making assertions. */
@module("react")
external act: (unit => promise<unit>) => promise<unit> = "act"
43 changes: 40 additions & 3 deletions src/ReactDOM.bs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

178 changes: 177 additions & 1 deletion src/ReactDOM.res
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,52 @@ module Client = {
external hydrateRoot: (Dom.element, React.element) => Root.t = "hydrateRoot"
}

// Very rudimentary form data bindings
module FormData = {
type t
type file

type formValue =
| String(string)
| File(file)

@new external make: unit => t = "FormData"

@send external append: (t, string, ~filename: string=?) => unit = "append"
@send external delete: (t, string) => unit = "delete"
@return(nullable) @send external getUnsafe: (t, string) => option<'a> = "get"
@send external getAllUnsafe: (t, string) => array<'a> = "getAll"

let getString = (formData, name) => {
switch formData->getUnsafe(name) {
| Some(value) => Js.typeof(value) === "string" ? Some(value) : None
| _ => None
}
}

external _asFile: 'a => file = "%identity"

let getFile = (formData, name) => {
switch formData->getUnsafe(name) {
| Some(value) => Js.typeof(value) === "string" ? None : Some(value->_asFile)
| _ => None
}
}

let getAll = (t, string) => {
t
->getAllUnsafe(string)
->Js.Array2.map(value => {
Js.typeof(value) === "string" ? String(value) : File(value->_asFile)
})
}

@send external set: (string, string) => unit = "set"
@send external has: string => bool = "has"
// @send external keys: t => Iterator.t<string> = "keys";
// @send external values: t => Iterator.t<value> = "values";
}

@module("react-dom")
external createPortal: (React.element, Dom.element) => React.element = "createPortal"

Expand All @@ -37,12 +83,142 @@ type domRef = JsxDOM.domRef
module Ref = {
type t = domRef
type currentDomRef = React.ref<Js.nullable<Dom.element>>
type callbackDomRef = Js.nullable<Dom.element> => unit
type callbackDomRef = Js.nullable<Dom.element> => option<unit => unit>

external domRef: currentDomRef => domRef = "%identity"
external callbackDomRef: callbackDomRef => domRef = "%identity"
}

// Hooks

type formStatus<'state> = {
/** If true, this means the parent <form> is pending submission. Otherwise, false. */
pending: bool,
/** An object implementing the FormData interface that contains the data the parent <form> is submitting. If there is no active submission or no parent <form>, it will be null. */
data: FormData.t,
/** This represents whether the parent <form> is submitting with either a GET or POST HTTP method. By default, a <form> will use the GET method and can be specified by the method property. */
method: [#get | #post],
/** A reference to the function passed to the action prop on the parent <form>. If there is no parent <form>, the property is null. If there is a URI value provided to the action prop, or no action prop specified, status.action will be null. */
action: React.action<'state, FormData.t>,
}

external formAction: React.formAction<FormData.t> => string = "%identity"

/** `useFormStatus` is a Hook that gives you status information of the last form submission. */
@module("react-dom")
external useFormStatus: unit => formStatus<'state> = "useFormStatus"

// Resource Preloading APIs

/** The CORS policy to use. */
type crossOrigin = [
| #anonymous
| #"use-credentials"
]

/** The Referrer header to send when fetching. */
type referrerPolicy = [
| #"referrer-when-downgrade"
| #"no-referrer"
| #origin
| #"origin-when-cross-origin"
| #"unsafe-url"
]

/** Suggests a relative priority for fetching the resource. */
type fetchPriority = [#auto | #high | #low]

/** `prefetchDNS` lets you eagerly look up the IP of a server that you expect to load resources from. */
@module("react-dom")
external prefetchDNS: string => unit = "prefetchDNS"

/** `preconnect` lets you eagerly connect to a server that you expect to load resources from. */
@module("react-dom")
external preconnect: string => unit = "preconnect"

type preloadOptions = {
/** The type of resource. */
@as("as")
as_: [
| #audio
| #document
| #embed
| #fetch
| #font
| #image
| #object
| #script
| #style
| #track
| #video
| #worker
],
/** The CORS policy to use. It is required when as is set to "fetch". */
crossOrigin?: crossOrigin,
/** The Referrer header to send when fetching. */
referrerPolicy?: referrerPolicy,
/** A cryptographic hash of the resource, to verify its authenticity. */
integrity?: string,
/** The MIME type of the resource. */
@as("type")
type_?: string,
/** A cryptographic nonce to allow the resource when using a strict Content Security Policy. */
nonce?: string,
/** Suggests a relative priority for fetching the resource. */
fetchPriority?: fetchPriority,
/** For use only with as: "image". Specifies the source set of the image. */
imageSrcSet?: string,
/** For use only with as: "image". Specifies the sizes of the image. */
imageSizes?: string,
}

/** `preload` lets you eagerly fetch a resource such as a stylesheet, font, or external script that you expect to use. */
@module("react-dom")
external preload: (string, preloadOptions) => unit = "preload"

type preloadModuleOptions = {
/** The type of resource. */
@as("as")
as_: [#script],
/** The CORS policy to use. It is required when as is set to "fetch". */
crossOrigin?: crossOrigin,
/** A cryptographic hash of the resource, to verify its authenticity. */
integrity?: string,
/** A cryptographic nonce to allow the resource when using a strict Content Security Policy. */
nonce?: string,
}

/** `preloadModule` lets you eagerly fetch an ESM module that you expect to use. */
@module("react-dom")
external preloadModule: (string, preloadModuleOptions) => unit = "preloadModule"

type preinitOptions = {
/** The type of resource. */
@as("as")
as_: [#script | #style],
/** Required with stylesheets. Says where to insert the stylesheet relative to others. Stylesheets with higher precedence can override those with lower precedence. */
precedence?: [#reset | #low | #medium | #high],
/** The CORS policy to use. It is required when as is set to "fetch". */
crossOrigin?: crossOrigin,
/** The Referrer header to send when fetching. */
referrerPolicy?: referrerPolicy,
/** A cryptographic hash of the resource, to verify its authenticity. */
integrity?: string,
nonce?: string,
/** Suggests a relative priority for fetching the resource. */
fetchPriority?: fetchPriority,
}

/** `preinit` lets you eagerly fetch and evaluate a stylesheet or external script. */
@module("react-dom")
external preinit: (string, preinitOptions) => unit = "preinit"

/** To preinit an ESM module, call the `preinitModule` function from react-dom. */
@module("react-dom")
external preinitModule: (string, preloadModuleOptions) => unit = "preinitModule"

// Runtime

type domProps = JsxDOM.domProps

@variadic @module("react")
Expand Down
2 changes: 2 additions & 0 deletions src/ReactDOMStatic.bs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions src/ReactDOMStatic.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
type abortSignal // WebAPI.EventAPI.abortSignal

type nodeStream // NodeJs.Stream.stream

type readableStream // WebAPI.FileAPI.readableStream

type prerenderOptions<'error> = {
bootstrapScriptContent?: string,
bootstrapScripts?: array<string>,
bootstrapModules?: array<string>,
identifierPrefix?: string,
namespaceURI?: string,
onError?: 'error => unit,
progressiveChunkSize?: int,
signal?: abortSignal,
}

type staticResult = {prelude: readableStream}

@module("react-dom/static")
external prerender: (React.element, ~options: prerenderOptions<'error>=?) => promise<staticResult> =
"prerender"

type staticResultNode = {prelude: nodeStream}

@module("react-dom/static")
external prerenderToNodeStream: (
React.element,
~options: prerenderOptions<'error>=?,
) => promise<staticResultNode> = "prerenderToNodeStream"