From 5094e0f03fff3481a3d520082b2e39bb46ef7bc4 Mon Sep 17 00:00:00 2001 From: lice Date: Fri, 1 Apr 2022 09:42:12 +0200 Subject: [PATCH 1/2] Add rider idea folder to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 5c5d96085fc4da8e7d11f5a729bb1928362a143a Mon Sep 17 00:00:00 2001 From: lice Date: Fri, 1 Apr 2022 14:56:45 +0200 Subject: [PATCH 2/2] Added react 18 root API support --- src/React.Core/IReactSiteConfiguration.cs | 12 +++ src/React.Core/ReactComponent.cs | 55 ++++++++-- src/React.Core/ReactSiteConfiguration.cs | 15 +++ tests/React.Tests/Core/ReactComponentTest.cs | 108 +++++++++++++++++++ 4 files changed, 184 insertions(+), 6 deletions(-) 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)]