Skip to content

Commit 78c2fcb

Browse files
cknittbloodyowlFreddy03h
authored
React 19 APIs (#133)
* React.promise * React.useTransition * React.useActionState * React.useOptimistic * React.use * React.act * React.useDeferredValue now takes initial value * ReactDOM: ref cleanup function * ReactDOM: Resource Preloading APIse * ReactDOM.useFormStatus * JS output changes * Add external for basic support for formAction * Add `usePromise` for `use(promise)` `use(context)` seems to exactly replicate the `useContext(context)` logic, and we want to maximize retro-compability (ie. not switch from `useContext` to `use` for that under the hood). simply adding `usePromise(promise)` seems to be the simplest, least invasive way to add the functionality. * proposal: make FormData more usable to get values this **kinda** goes against the zero-cost philosophy, but I don't see a world where users would not have to reimplement those. * useOptimistic optionnal updateFn * dom static prerender and prerenderToNodeStream --------- Co-authored-by: Matthias Le Brun <[email protected]> Co-authored-by: Freddy Harris <[email protected]>
1 parent 2f7a5fa commit 78c2fcb

File tree

6 files changed

+288
-10
lines changed

6 files changed

+288
-10
lines changed

src/React.bs.js

-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/React.res

+39-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type element = Jsx.element
55
external float: float => element = "%identity"
66
external int: int => element = "%identity"
77
external string: string => element = "%identity"
8+
external promise: promise<element> => element = "%identity"
89

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

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

254+
@module("react")
255+
external usePromise: promise<'a> => 'a = "use"
256+
253257
@module("react") external useRef: 'value => ref<'value> = "useRef"
254258

255259
@module("react")
@@ -309,10 +313,9 @@ external useImperativeHandle7: (
309313

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

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

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

406409
@get @return(nullable)
407410
external displayName: component<'props> => option<string> = "displayName"
411+
412+
// Actions
413+
414+
type transitionFunction = unit => promise<unit>
415+
416+
type transitionStartFunction = transitionFunction => unit
417+
418+
/** `useTransition` is a React Hook that lets you render a part of the UI in the background. */
419+
@module("react")
420+
external useTransition: unit => (bool, transitionStartFunction) = "useTransition"
421+
422+
type action<'state, 'payload> = ('state, 'payload) => promise<'state>
423+
424+
type formAction<'formData> = 'formData => promise<unit>
425+
426+
/** `useActionState` is a Hook that allows you to update state based on the result of a form action. */
427+
@module("react")
428+
external useActionState: (
429+
action<'state, 'payload>,
430+
'state,
431+
~permalink: string=?,
432+
) => ('state, formAction<'payload>, bool) = "useActionState"
433+
434+
/** `useOptimistic` is a React Hook that lets you optimistically update the UI. */
435+
@module("react")
436+
external useOptimistic: (
437+
'state,
438+
~updateFn: ('state, 'action) => 'state=?,
439+
) => ('state, 'action => unit) = "useOptimistic"
440+
441+
/** `act` is a test helper to apply pending React updates before making assertions. */
442+
@module("react")
443+
external act: (unit => promise<unit>) => promise<unit> = "act"

src/ReactDOM.bs.js

+40-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ReactDOM.res

+177-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,52 @@ module Client = {
2525
external hydrateRoot: (Dom.element, React.element) => Root.t = "hydrateRoot"
2626
}
2727

28+
// Very rudimentary form data bindings
29+
module FormData = {
30+
type t
31+
type file
32+
33+
type formValue =
34+
| String(string)
35+
| File(file)
36+
37+
@new external make: unit => t = "FormData"
38+
39+
@send external append: (t, string, ~filename: string=?) => unit = "append"
40+
@send external delete: (t, string) => unit = "delete"
41+
@return(nullable) @send external getUnsafe: (t, string) => option<'a> = "get"
42+
@send external getAllUnsafe: (t, string) => array<'a> = "getAll"
43+
44+
let getString = (formData, name) => {
45+
switch formData->getUnsafe(name) {
46+
| Some(value) => Js.typeof(value) === "string" ? Some(value) : None
47+
| _ => None
48+
}
49+
}
50+
51+
external _asFile: 'a => file = "%identity"
52+
53+
let getFile = (formData, name) => {
54+
switch formData->getUnsafe(name) {
55+
| Some(value) => Js.typeof(value) === "string" ? None : Some(value->_asFile)
56+
| _ => None
57+
}
58+
}
59+
60+
let getAll = (t, string) => {
61+
t
62+
->getAllUnsafe(string)
63+
->Js.Array2.map(value => {
64+
Js.typeof(value) === "string" ? String(value) : File(value->_asFile)
65+
})
66+
}
67+
68+
@send external set: (string, string) => unit = "set"
69+
@send external has: string => bool = "has"
70+
// @send external keys: t => Iterator.t<string> = "keys";
71+
// @send external values: t => Iterator.t<value> = "values";
72+
}
73+
2874
@module("react-dom")
2975
external createPortal: (React.element, Dom.element) => React.element = "createPortal"
3076

@@ -37,12 +83,142 @@ type domRef = JsxDOM.domRef
3783
module Ref = {
3884
type t = domRef
3985
type currentDomRef = React.ref<Js.nullable<Dom.element>>
40-
type callbackDomRef = Js.nullable<Dom.element> => unit
86+
type callbackDomRef = Js.nullable<Dom.element> => option<unit => unit>
4187

4288
external domRef: currentDomRef => domRef = "%identity"
4389
external callbackDomRef: callbackDomRef => domRef = "%identity"
4490
}
4591

92+
// Hooks
93+
94+
type formStatus<'state> = {
95+
/** If true, this means the parent <form> is pending submission. Otherwise, false. */
96+
pending: bool,
97+
/** 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. */
98+
data: FormData.t,
99+
/** 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. */
100+
method: [#get | #post],
101+
/** 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. */
102+
action: React.action<'state, FormData.t>,
103+
}
104+
105+
external formAction: React.formAction<FormData.t> => string = "%identity"
106+
107+
/** `useFormStatus` is a Hook that gives you status information of the last form submission. */
108+
@module("react-dom")
109+
external useFormStatus: unit => formStatus<'state> = "useFormStatus"
110+
111+
// Resource Preloading APIs
112+
113+
/** The CORS policy to use. */
114+
type crossOrigin = [
115+
| #anonymous
116+
| #"use-credentials"
117+
]
118+
119+
/** The Referrer header to send when fetching. */
120+
type referrerPolicy = [
121+
| #"referrer-when-downgrade"
122+
| #"no-referrer"
123+
| #origin
124+
| #"origin-when-cross-origin"
125+
| #"unsafe-url"
126+
]
127+
128+
/** Suggests a relative priority for fetching the resource. */
129+
type fetchPriority = [#auto | #high | #low]
130+
131+
/** `prefetchDNS` lets you eagerly look up the IP of a server that you expect to load resources from. */
132+
@module("react-dom")
133+
external prefetchDNS: string => unit = "prefetchDNS"
134+
135+
/** `preconnect` lets you eagerly connect to a server that you expect to load resources from. */
136+
@module("react-dom")
137+
external preconnect: string => unit = "preconnect"
138+
139+
type preloadOptions = {
140+
/** The type of resource. */
141+
@as("as")
142+
as_: [
143+
| #audio
144+
| #document
145+
| #embed
146+
| #fetch
147+
| #font
148+
| #image
149+
| #object
150+
| #script
151+
| #style
152+
| #track
153+
| #video
154+
| #worker
155+
],
156+
/** The CORS policy to use. It is required when as is set to "fetch". */
157+
crossOrigin?: crossOrigin,
158+
/** The Referrer header to send when fetching. */
159+
referrerPolicy?: referrerPolicy,
160+
/** A cryptographic hash of the resource, to verify its authenticity. */
161+
integrity?: string,
162+
/** The MIME type of the resource. */
163+
@as("type")
164+
type_?: string,
165+
/** A cryptographic nonce to allow the resource when using a strict Content Security Policy. */
166+
nonce?: string,
167+
/** Suggests a relative priority for fetching the resource. */
168+
fetchPriority?: fetchPriority,
169+
/** For use only with as: "image". Specifies the source set of the image. */
170+
imageSrcSet?: string,
171+
/** For use only with as: "image". Specifies the sizes of the image. */
172+
imageSizes?: string,
173+
}
174+
175+
/** `preload` lets you eagerly fetch a resource such as a stylesheet, font, or external script that you expect to use. */
176+
@module("react-dom")
177+
external preload: (string, preloadOptions) => unit = "preload"
178+
179+
type preloadModuleOptions = {
180+
/** The type of resource. */
181+
@as("as")
182+
as_: [#script],
183+
/** The CORS policy to use. It is required when as is set to "fetch". */
184+
crossOrigin?: crossOrigin,
185+
/** A cryptographic hash of the resource, to verify its authenticity. */
186+
integrity?: string,
187+
/** A cryptographic nonce to allow the resource when using a strict Content Security Policy. */
188+
nonce?: string,
189+
}
190+
191+
/** `preloadModule` lets you eagerly fetch an ESM module that you expect to use. */
192+
@module("react-dom")
193+
external preloadModule: (string, preloadModuleOptions) => unit = "preloadModule"
194+
195+
type preinitOptions = {
196+
/** The type of resource. */
197+
@as("as")
198+
as_: [#script | #style],
199+
/** Required with stylesheets. Says where to insert the stylesheet relative to others. Stylesheets with higher precedence can override those with lower precedence. */
200+
precedence?: [#reset | #low | #medium | #high],
201+
/** The CORS policy to use. It is required when as is set to "fetch". */
202+
crossOrigin?: crossOrigin,
203+
/** The Referrer header to send when fetching. */
204+
referrerPolicy?: referrerPolicy,
205+
/** A cryptographic hash of the resource, to verify its authenticity. */
206+
integrity?: string,
207+
nonce?: string,
208+
/** Suggests a relative priority for fetching the resource. */
209+
fetchPriority?: fetchPriority,
210+
}
211+
212+
/** `preinit` lets you eagerly fetch and evaluate a stylesheet or external script. */
213+
@module("react-dom")
214+
external preinit: (string, preinitOptions) => unit = "preinit"
215+
216+
/** To preinit an ESM module, call the `preinitModule` function from react-dom. */
217+
@module("react-dom")
218+
external preinitModule: (string, preloadModuleOptions) => unit = "preinitModule"
219+
220+
// Runtime
221+
46222
type domProps = JsxDOM.domProps
47223

48224
@variadic @module("react")

src/ReactDOMStatic.bs.js

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ReactDOMStatic.res

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
type abortSignal // WebAPI.EventAPI.abortSignal
2+
3+
type nodeStream // NodeJs.Stream.stream
4+
5+
type readableStream // WebAPI.FileAPI.readableStream
6+
7+
type prerenderOptions<'error> = {
8+
bootstrapScriptContent?: string,
9+
bootstrapScripts?: array<string>,
10+
bootstrapModules?: array<string>,
11+
identifierPrefix?: string,
12+
namespaceURI?: string,
13+
onError?: 'error => unit,
14+
progressiveChunkSize?: int,
15+
signal?: abortSignal,
16+
}
17+
18+
type staticResult = {prelude: readableStream}
19+
20+
@module("react-dom/static")
21+
external prerender: (React.element, ~options: prerenderOptions<'error>=?) => promise<staticResult> =
22+
"prerender"
23+
24+
type staticResultNode = {prelude: nodeStream}
25+
26+
@module("react-dom/static")
27+
external prerenderToNodeStream: (
28+
React.element,
29+
~options: prerenderOptions<'error>=?,
30+
) => promise<staticResultNode> = "prerenderToNodeStream"

0 commit comments

Comments
 (0)