diff --git a/.gitignore b/.gitignore
index b32bd32fb..cec4815ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -190,3 +190,4 @@ UpgradeLog*.htm
# Microsoft Fakes
FakesAssemblies/
+src/.idea
diff --git a/src/React.Core/IReactSiteConfiguration.cs b/src/React.Core/IReactSiteConfiguration.cs
index 5bd18d0b4..3d91335cc 100644
--- a/src/React.Core/IReactSiteConfiguration.cs
+++ b/src/React.Core/IReactSiteConfiguration.cs
@@ -228,5 +228,17 @@ public interface IReactSiteConfiguration
///
///
IReactSiteConfiguration SetReactAppBuildPath(string reactAppBuildPath);
+
+ ///
+ /// Gets or sets if the React 18+ create root api should be used for rendering / hydration.
+ /// If false ReactDOM.render / ReactDOM.hydrate will be used.
+ ///
+ bool UseRootAPI { get; set; }
+
+ ///
+ /// Enables usage of the React 18 root API when rendering / hydrating.
+ ///
+ ///
+ void EnableReact18RootAPI();
}
}
diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs
index 9b54f10ff..2c0af0c55 100644
--- a/src/React.Core/ReactComponent.cs
+++ b/src/React.Core/ReactComponent.cs
@@ -251,12 +251,14 @@ public virtual void RenderJavaScript(TextWriter writer, bool waitForDOMContentLo
writer.Write("window.addEventListener('DOMContentLoaded', function() {");
}
- writer.Write(
- !_configuration.UseServerSideRendering || ClientOnly ? "ReactDOM.render(" : "ReactDOM.hydrate(");
- WriteComponentInitialiser(writer);
- writer.Write(", document.getElementById(\"");
- writer.Write(ContainerId);
- writer.Write("\"))");
+ if (_configuration.UseRootAPI)
+ {
+ WriteComponentInitialization(writer);
+ }
+ else
+ {
+ WriteLegacyComponentInitialization(writer);
+ }
if (waitForDOMContentLoad)
{
@@ -264,6 +266,47 @@ public virtual void RenderJavaScript(TextWriter writer, bool waitForDOMContentLo
}
}
+ ///
+ /// Writes initialization code using the React 18 root API
+ ///
+ private void WriteComponentInitialization(TextWriter writer)
+ {
+ var hydrate = _configuration.UseServerSideRendering && !ClientOnly;
+ if (hydrate)
+ {
+ writer.Write("ReactDOM.hydrateRoot(");
+ writer.Write("document.getElementById(\"");
+ writer.Write(ContainerId);
+ writer.Write("\")");
+ writer.Write(", ");
+ WriteComponentInitialiser(writer);
+ writer.Write(")");
+ }
+ else
+ {
+ writer.Write("ReactDOM.createRoot(");
+ writer.Write("document.getElementById(\"");
+ writer.Write(ContainerId);
+ writer.Write("\"))");
+ writer.Write(".render(");
+ WriteComponentInitialiser(writer);
+ writer.Write(")");
+ }
+ }
+
+ ///
+ /// Writes initialization code using the old ReactDOM.render / ReactDOM.hydrate APIs.
+ ///
+ private void WriteLegacyComponentInitialization(TextWriter writer)
+ {
+ writer.Write(
+ !_configuration.UseServerSideRendering || ClientOnly ? "ReactDOM.render(" : "ReactDOM.hydrate(");
+ WriteComponentInitialiser(writer);
+ writer.Write(", document.getElementById(\"");
+ writer.Write(ContainerId);
+ writer.Write("\"))");
+ }
+
///
/// Ensures that this component exists in global scope
///
diff --git a/src/React.Core/ReactSiteConfiguration.cs b/src/React.Core/ReactSiteConfiguration.cs
index e4b36d29c..4136601a6 100644
--- a/src/React.Core/ReactSiteConfiguration.cs
+++ b/src/React.Core/ReactSiteConfiguration.cs
@@ -375,5 +375,20 @@ public IReactSiteConfiguration SetReactAppBuildPath(string reactAppBuildPath)
ReactAppBuildPath = reactAppBuildPath;
return this;
}
+
+ ///
+ /// Gets or sets if the React 18+ create root api should be used for rendering / hydration.
+ /// If false ReactDOM.render / ReactDOM.hydrate will be used.
+ ///
+ public bool UseRootAPI { get; set; }
+
+ ///
+ /// Enables usage of the React 18 root API when rendering / hydrating.
+ ///
+ ///
+ public void EnableReact18RootAPI()
+ {
+ UseRootAPI = true;
+ }
}
}
diff --git a/tests/React.Tests/Core/ReactComponentTest.cs b/tests/React.Tests/Core/ReactComponentTest.cs
index 1a0cbc93f..0be5ac41d 100644
--- a/tests/React.Tests/Core/ReactComponentTest.cs
+++ b/tests/React.Tests/Core/ReactComponentTest.cs
@@ -296,6 +296,114 @@ public void RenderJavaScriptShouldHandleWaitForContentLoad()
);
}
}
+
+ [Fact]
+ public void RenderJavaScriptShouldCallRenderComponentUsingRootAPI()
+ {
+ var environment = new Mock();
+ var config = CreateDefaultConfigMock();
+ config.SetupGet(x => x.UseRootAPI).Returns(true);
+ var reactIdGenerator = new Mock();
+
+ var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
+ {
+ Props = new { hello = "World" }
+ };
+ var result = component.RenderJavaScript(false);
+
+ Assert.Equal(
+ @"ReactDOM.hydrateRoot(document.getElementById(""container""), React.createElement(Foo, {""hello"":""World""}))",
+ result
+ );
+ }
+
+ [Fact]
+ public void RenderJavaScriptShouldCallRenderComponentWithRootRender()
+ {
+ var environment = new Mock();
+ var config = CreateDefaultConfigMock();
+ config.SetupGet(x => x.UseRootAPI).Returns(true);
+ var reactIdGenerator = new Mock();
+
+ var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
+ {
+ ClientOnly = true,
+ Props = new { hello = "World" }
+ };
+ var result = component.RenderJavaScript(false);
+
+ Assert.Equal(
+ @"ReactDOM.createRoot(document.getElementById(""container"")).render(React.createElement(Foo, {""hello"":""World""}))",
+ result
+ );
+ }
+
+ [Fact]
+ public void RenderJavaScriptShouldCallRenderComponentwithReactDOMHydrateRoot()
+ {
+ var environment = new Mock();
+ var config = CreateDefaultConfigMock();
+ config.SetupGet(x => x.UseRootAPI).Returns(true);
+ var reactIdGenerator = new Mock();
+
+ var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
+ {
+ ClientOnly = false,
+ Props = new { hello = "World" }
+ };
+ var result = component.RenderJavaScript(false);
+
+ Assert.Equal(
+ @"ReactDOM.hydrateRoot(document.getElementById(""container""), React.createElement(Foo, {""hello"":""World""}))",
+ result
+ );
+ }
+
+ [Fact]
+ public void RenderJavaScriptShouldCallRootRenderWhenSsrDisabled()
+ {
+ var environment = new Mock();
+ var config = CreateDefaultConfigMock();
+ config.SetupGet(x => x.UseServerSideRendering).Returns(false);
+ config.SetupGet(x => x.UseRootAPI).Returns(true);
+
+ var reactIdGenerator = new Mock();
+ var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
+ {
+ ClientOnly = false,
+ Props = new {hello = "World"}
+ };
+ var result = component.RenderJavaScript(false);
+
+ Assert.Equal(
+ @"ReactDOM.createRoot(document.getElementById(""container"")).render(React.createElement(Foo, {""hello"":""World""}))",
+ result
+ );
+ }
+
+ [Fact]
+ public void RenderJavaScriptShouldHandleWaitForContentLoadWhenUsingRootAPI()
+ {
+ var environment = new Mock();
+ var config = CreateDefaultConfigMock();
+ config.SetupGet(x => x.UseServerSideRendering).Returns(false);
+ config.SetupGet(x => x.UseRootAPI).Returns(true);
+
+ var reactIdGenerator = new Mock();
+ var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
+ {
+ ClientOnly = false,
+ Props = new {hello = "World"}
+ };
+ using (var writer = new StringWriter())
+ {
+ component.RenderJavaScript(writer, waitForDOMContentLoad: true);
+ Assert.Equal(
+ @"window.addEventListener('DOMContentLoaded', function() {ReactDOM.createRoot(document.getElementById(""container"")).render(React.createElement(Foo, {""hello"":""World""}))});",
+ writer.ToString()
+ );
+ }
+ }
[Theory]
[InlineData("Foo", true)]