Skip to content

Commit 5c5d960

Browse files
Added react 18 root API support
1 parent 5094e0f commit 5c5d960

File tree

4 files changed

+184
-6
lines changed

4 files changed

+184
-6
lines changed

src/React.Core/IReactSiteConfiguration.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,5 +228,17 @@ public interface IReactSiteConfiguration
228228
/// <param name="reactAppBuildPath"></param>
229229
/// <returns></returns>
230230
IReactSiteConfiguration SetReactAppBuildPath(string reactAppBuildPath);
231+
232+
/// <summary>
233+
/// Gets or sets if the React 18+ create root api should be used for rendering / hydration.
234+
/// If false ReactDOM.render / ReactDOM.hydrate will be used.
235+
/// </summary>
236+
bool UseRootAPI { get; set; }
237+
238+
/// <summary>
239+
/// Enables usage of the React 18 root API when rendering / hydrating.
240+
/// </summary>
241+
/// <returns></returns>
242+
void EnableReact18RootAPI();
231243
}
232244
}

src/React.Core/ReactComponent.cs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,19 +251,62 @@ public virtual void RenderJavaScript(TextWriter writer, bool waitForDOMContentLo
251251
writer.Write("window.addEventListener('DOMContentLoaded', function() {");
252252
}
253253

254-
writer.Write(
255-
!_configuration.UseServerSideRendering || ClientOnly ? "ReactDOM.render(" : "ReactDOM.hydrate(");
256-
WriteComponentInitialiser(writer);
257-
writer.Write(", document.getElementById(\"");
258-
writer.Write(ContainerId);
259-
writer.Write("\"))");
254+
if (_configuration.UseRootAPI)
255+
{
256+
WriteComponentInitialization(writer);
257+
}
258+
else
259+
{
260+
WriteLegacyComponentInitialization(writer);
261+
}
260262

261263
if (waitForDOMContentLoad)
262264
{
263265
writer.Write("});");
264266
}
265267
}
266268

269+
/// <summary>
270+
/// Writes initialization code using the React 18 root API
271+
/// </summary>
272+
private void WriteComponentInitialization(TextWriter writer)
273+
{
274+
var hydrate = _configuration.UseServerSideRendering && !ClientOnly;
275+
if (hydrate)
276+
{
277+
writer.Write("ReactDOM.hydrateRoot(");
278+
writer.Write("document.getElementById(\"");
279+
writer.Write(ContainerId);
280+
writer.Write("\")");
281+
writer.Write(", ");
282+
WriteComponentInitialiser(writer);
283+
writer.Write(")");
284+
}
285+
else
286+
{
287+
writer.Write("ReactDOM.createRoot(");
288+
writer.Write("document.getElementById(\"");
289+
writer.Write(ContainerId);
290+
writer.Write("\"))");
291+
writer.Write(".render(");
292+
WriteComponentInitialiser(writer);
293+
writer.Write(")");
294+
}
295+
}
296+
297+
/// <summary>
298+
/// Writes initialization code using the old ReactDOM.render / ReactDOM.hydrate APIs.
299+
/// </summary>
300+
private void WriteLegacyComponentInitialization(TextWriter writer)
301+
{
302+
writer.Write(
303+
!_configuration.UseServerSideRendering || ClientOnly ? "ReactDOM.render(" : "ReactDOM.hydrate(");
304+
WriteComponentInitialiser(writer);
305+
writer.Write(", document.getElementById(\"");
306+
writer.Write(ContainerId);
307+
writer.Write("\"))");
308+
}
309+
267310
/// <summary>
268311
/// Ensures that this component exists in global scope
269312
/// </summary>

src/React.Core/ReactSiteConfiguration.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,5 +375,20 @@ public IReactSiteConfiguration SetReactAppBuildPath(string reactAppBuildPath)
375375
ReactAppBuildPath = reactAppBuildPath;
376376
return this;
377377
}
378+
379+
/// <summary>
380+
/// Gets or sets if the React 18+ create root api should be used for rendering / hydration.
381+
/// If false ReactDOM.render / ReactDOM.hydrate will be used.
382+
/// </summary>
383+
public bool UseRootAPI { get; set; }
384+
385+
/// <summary>
386+
/// Enables usage of the React 18 root API when rendering / hydrating.
387+
/// </summary>
388+
/// <returns></returns>
389+
public void EnableReact18RootAPI()
390+
{
391+
UseRootAPI = true;
392+
}
378393
}
379394
}

tests/React.Tests/Core/ReactComponentTest.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,114 @@ public void RenderJavaScriptShouldHandleWaitForContentLoad()
296296
);
297297
}
298298
}
299+
300+
[Fact]
301+
public void RenderJavaScriptShouldCallRenderComponentUsingRootAPI()
302+
{
303+
var environment = new Mock<IReactEnvironment>();
304+
var config = CreateDefaultConfigMock();
305+
config.SetupGet(x => x.UseRootAPI).Returns(true);
306+
var reactIdGenerator = new Mock<IReactIdGenerator>();
307+
308+
var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
309+
{
310+
Props = new { hello = "World" }
311+
};
312+
var result = component.RenderJavaScript(false);
313+
314+
Assert.Equal(
315+
@"ReactDOM.hydrateRoot(document.getElementById(""container""), React.createElement(Foo, {""hello"":""World""}))",
316+
result
317+
);
318+
}
319+
320+
[Fact]
321+
public void RenderJavaScriptShouldCallRenderComponentWithRootRender()
322+
{
323+
var environment = new Mock<IReactEnvironment>();
324+
var config = CreateDefaultConfigMock();
325+
config.SetupGet(x => x.UseRootAPI).Returns(true);
326+
var reactIdGenerator = new Mock<IReactIdGenerator>();
327+
328+
var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
329+
{
330+
ClientOnly = true,
331+
Props = new { hello = "World" }
332+
};
333+
var result = component.RenderJavaScript(false);
334+
335+
Assert.Equal(
336+
@"ReactDOM.createRoot(document.getElementById(""container"")).render(React.createElement(Foo, {""hello"":""World""}))",
337+
result
338+
);
339+
}
340+
341+
[Fact]
342+
public void RenderJavaScriptShouldCallRenderComponentwithReactDOMHydrateRoot()
343+
{
344+
var environment = new Mock<IReactEnvironment>();
345+
var config = CreateDefaultConfigMock();
346+
config.SetupGet(x => x.UseRootAPI).Returns(true);
347+
var reactIdGenerator = new Mock<IReactIdGenerator>();
348+
349+
var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
350+
{
351+
ClientOnly = false,
352+
Props = new { hello = "World" }
353+
};
354+
var result = component.RenderJavaScript(false);
355+
356+
Assert.Equal(
357+
@"ReactDOM.hydrateRoot(document.getElementById(""container""), React.createElement(Foo, {""hello"":""World""}))",
358+
result
359+
);
360+
}
361+
362+
[Fact]
363+
public void RenderJavaScriptShouldCallRootRenderWhenSsrDisabled()
364+
{
365+
var environment = new Mock<IReactEnvironment>();
366+
var config = CreateDefaultConfigMock();
367+
config.SetupGet(x => x.UseServerSideRendering).Returns(false);
368+
config.SetupGet(x => x.UseRootAPI).Returns(true);
369+
370+
var reactIdGenerator = new Mock<IReactIdGenerator>();
371+
var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
372+
{
373+
ClientOnly = false,
374+
Props = new {hello = "World"}
375+
};
376+
var result = component.RenderJavaScript(false);
377+
378+
Assert.Equal(
379+
@"ReactDOM.createRoot(document.getElementById(""container"")).render(React.createElement(Foo, {""hello"":""World""}))",
380+
result
381+
);
382+
}
383+
384+
[Fact]
385+
public void RenderJavaScriptShouldHandleWaitForContentLoadWhenUsingRootAPI()
386+
{
387+
var environment = new Mock<IReactEnvironment>();
388+
var config = CreateDefaultConfigMock();
389+
config.SetupGet(x => x.UseServerSideRendering).Returns(false);
390+
config.SetupGet(x => x.UseRootAPI).Returns(true);
391+
392+
var reactIdGenerator = new Mock<IReactIdGenerator>();
393+
var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
394+
{
395+
ClientOnly = false,
396+
Props = new {hello = "World"}
397+
};
398+
using (var writer = new StringWriter())
399+
{
400+
component.RenderJavaScript(writer, waitForDOMContentLoad: true);
401+
Assert.Equal(
402+
@"window.addEventListener('DOMContentLoaded', function() {ReactDOM.createRoot(document.getElementById(""container"")).render(React.createElement(Foo, {""hello"":""World""}))});",
403+
writer.ToString()
404+
);
405+
}
406+
}
299407

300408
[Theory]
301409
[InlineData("Foo", true)]

0 commit comments

Comments
 (0)