Skip to content

Commit 375f5a0

Browse files
committed
Merge branch 'main' into feat/model-values
2 parents 2272c66 + 3f55d4f commit 375f5a0

File tree

3 files changed

+78
-12
lines changed

3 files changed

+78
-12
lines changed

packages/reactDom/src/ReactServerDOM.ml

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ module Stream = struct
4343
Lwt.return ());
4444
index
4545

46-
let make ~initial_index =
46+
let make ?(initial_index = 0) ?(pending = 0) () =
4747
let stream, push, close = Push_stream.make () in
48-
(stream, { push; close; pending = 0; index = initial_index })
48+
(stream, { push; close; pending; index = initial_index })
4949
end
5050

5151
(* Resources module maintains insertion order while deduplicating based on src/href *)
@@ -384,7 +384,7 @@ module Model = struct
384384
List.map (fun (name, value) -> (name, model_to_payload ~context ~is_root:false ~to_chunk ~env value)) props
385385

386386
let render ?(env = `Dev) ?(debug = false) ?subscribe model =
387-
let stream, context = Stream.make ~initial_index:0 in
387+
let stream, context = Stream.make () in
388388
let to_root_chunk model id =
389389
let payload = model_to_payload ~debug ~is_root:true ~context ~to_chunk ~env model in
390390
to_chunk (Value payload) id
@@ -403,7 +403,7 @@ module Model = struct
403403
let digest = stack |> Hashtbl.hash |> Int.to_string in
404404
Lwt.return (React.Model.Error { message; stack; env = "Server"; digest })
405405
in
406-
let stream, context = Stream.make ~initial_index:0 in
406+
let stream, context = Stream.make () in
407407
let to_root_chunk value id =
408408
let payload = model_to_payload ~debug ~is_root:true ~context ~to_chunk ~env value in
409409
to_chunk (Value payload) id
@@ -497,11 +497,9 @@ let rec client_to_html ~(fiber : Fiber.t) (element : React.element) =
497497
| output -> client_to_html ~fiber output
498498
in
499499
wait_for_suspense_to_resolve ()
500-
| Async_component (_, _component) ->
501-
(* async components can't be interleaved in client components, for now *)
502-
raise
503-
(Invalid_argument
504-
"async components can't be part of a client component. This should never raise, the ppx should catch it")
500+
| Async_component (_, component) ->
501+
let%lwt element = component () in
502+
client_to_html ~fiber element
505503
| Suspense { key = _; children; fallback } ->
506504
(* TODO: Do we need to care if there's Any_promise raising ? *)
507505
let%lwt fallback = client_to_html ~fiber fallback in
@@ -767,8 +765,13 @@ let render_html ?(skipRoot = false) ?(env = `Dev) ?debug:(_ = false) ?bootstrapS
767765
| None -> [])
768766
in
769767
(* Since we don't push the root_data_payload to the stream but return it immediately with the initial HTML,
770-
the stream's initial index starts at 1, with index 0 reserved for the root_data_payload. *)
771-
let stream, context = Stream.make ~initial_index:1 in
768+
the stream's initial index starts at 1, with index 0 reserved for the root_data_payload.
769+
770+
The root is also treated as a pending segment that must complete before the stream can be closed,
771+
as we don't push_async it to the stream, the pending counter starts at 1.
772+
Similar on how react does: https://github.com/facebook/react/blob/7d9f876cbc7e9363092e60436704cf8ae435b969/packages/react-server/src/ReactFizzServer.js#L572-L581
773+
*)
774+
let stream, context = Stream.make ~initial_index:1 ~pending:1 () in
772775
let fiber : Fiber.t =
773776
{
774777
context;
@@ -785,8 +788,10 @@ let render_html ?(skipRoot = false) ?(env = `Dev) ?debug:(_ = false) ?bootstrapS
785788
let%lwt root_html, root_model = render_element_to_html ~fiber element in
786789
(* To return the model value immediately, we don't push it to the stream but return it as a payload script together with the user_scripts *)
787790
let root_data_payload = model_to_chunk (Value root_model) 0 in
791+
(* Decrement the pending counter to signal that the root data payload is complete. *)
792+
context.pending <- context.pending - 1;
788793
(* In case of not having any task pending, we can close the stream *)
789-
(match context.pending = 0 with true -> context.close () | false -> ());
794+
if context.pending = 0 then context.close ();
790795
let bootstrap_script_content =
791796
match bootstrapScriptContent with
792797
| None -> Html.null

packages/reactDom/test/test_RSC_html.ml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,35 @@ let client_with_element_props () =
349349
'>window.srr_stream.push()</script>";
350350
]
351351

352+
let client_component_with_async_component () =
353+
let children =
354+
React.Async_component
355+
( __FUNCTION__,
356+
fun () ->
357+
let%lwt () = sleep ~ms:10 in
358+
Lwt.return (React.string "Async Component") )
359+
in
360+
let app ~children =
361+
React.Upper_case_component
362+
( "app",
363+
fun () ->
364+
React.Client_component
365+
{
366+
import_module = "./client.js";
367+
import_name = "Client";
368+
props = [ ("children", React.Model.Element children) ];
369+
client = children;
370+
} )
371+
in
372+
assert_html (app ~children)
373+
~shell:
374+
"Async Component<script data-payload='0:[\"$\",\"$2\",null,{\"children\":\"$L1\"},null,[],{}]\n\
375+
'>window.srr_stream.push()</script>"
376+
[
377+
"<script data-payload='1:\"Async Component\"\n'>window.srr_stream.push()</script>";
378+
"<script data-payload='2:I[\"./client.js\",[],\"Client\"]\n'>window.srr_stream.push()</script>";
379+
]
380+
352381
let suspense_with_error () =
353382
let app () =
354383
React.Suspense.make ~fallback:(React.string "Loading...")
@@ -641,6 +670,7 @@ let tests =
641670
test "suspense_without_promise" suspense_without_promise;
642671
test "with_sleepy_promise" with_sleepy_promise;
643672
test "client_with_promise_props" client_with_promise_props;
673+
test "client_component_with_async_component" client_component_with_async_component;
644674
test "async_component_with_promise" async_component_with_promise;
645675
test "suspense_with_error" suspense_with_error;
646676
test "suspense_with_error_in_async" suspense_with_error_in_async;

packages/reactDom/test/test_RSC_model.ml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,36 @@ let client_component_with_resources_metadata () =
814814
Alcotest.(check bool) "should have head with resources" true has_head_with_resources;
815815
Lwt.return ()
816816

817+
let client_component_with_async_component () =
818+
let async_component =
819+
React.Async_component
820+
( __FUNCTION__,
821+
fun () ->
822+
let%lwt () = sleep ~ms:10 in
823+
Lwt.return (React.string "Async Component") )
824+
in
825+
let app ~children =
826+
React.Upper_case_component
827+
( "app",
828+
fun () ->
829+
React.Client_component
830+
{
831+
import_module = "./client.js";
832+
import_name = "Client";
833+
props = [ ("children", React.Model.Element children) ];
834+
client = children;
835+
} )
836+
in
837+
let output, subscribe = capture_stream () in
838+
let%lwt () = ReactServerDOM.render_model ~subscribe (React.Model.Element (app ~children:async_component)) in
839+
assert_list_of_strings !output
840+
[
841+
"1:I[\"./client.js\",[],\"Client\"]\n";
842+
"0:[\"$\",\"$1\",null,{\"children\":\"$L2\"},null,[],{}]\n";
843+
"2:\"Async Component\"\n";
844+
];
845+
Lwt.return ()
846+
817847
let page_with_hoisted_resources () =
818848
(* Test that resources like scripts and styles are properly hoisted *)
819849
let app () =
@@ -981,6 +1011,7 @@ let tests =
9811011
test "client_without_props" client_without_props;
9821012
test "client_with_element_props" client_with_element_props;
9831013
test "client_with_server_children" client_with_server_children;
1014+
test "client_component_with_async_component" client_component_with_async_component;
9841015
test "act_with_simple_response" act_with_simple_response;
9851016
test "env_development_adds_debug_info" env_development_adds_debug_info;
9861017
test "act_with_error" act_with_error;

0 commit comments

Comments
 (0)