Skip to content

Commit e5cb5d8

Browse files
Gustavdustinsoftware
Gustav
authored andcommitted
Improve server side only rendering (#497)
1 parent 0782582 commit e5cb5d8

8 files changed

+96
-22
lines changed

src/React.AspNet/HtmlHelperExtensions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ private static IReactEnvironment Environment
5454
/// <param name="htmlTag">HTML tag to wrap the component in. Defaults to &lt;div&gt;</param>
5555
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
5656
/// <param name="clientOnly">Skip rendering server-side and only output client-side initialisation code. Defaults to <c>false</c></param>
57-
/// <param name="serverOnly">Skip rendering React specific data-attributes during server side rendering. Defaults to <c>false</c></param>
57+
/// <param name="serverOnly">Skip rendering React specific data-attributes, container and client-side initialisation during server side rendering. Defaults to <c>false</c></param>
5858
/// <param name="containerClass">HTML class(es) to set on the container tag</param>
5959
/// <param name="exceptionHandler">A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)</param>
6060
/// <returns>The component's HTML</returns>
@@ -72,7 +72,7 @@ public static IHtmlString React<T>(
7272
{
7373
try
7474
{
75-
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly);
75+
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly, serverOnly);
7676
if (!string.IsNullOrEmpty(htmlTag))
7777
{
7878
reactComponent.ContainerTag = htmlTag;

src/React.Core/IReactComponent.cs

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ public interface IReactComponent
4141
/// </summary>
4242
string ContainerClass { get; set; }
4343

44+
/// <summary>
45+
/// Get or sets if this components only should be rendered server side
46+
/// </summary>
47+
bool ServerOnly { get; set; }
48+
4449
/// <summary>
4550
/// Renders the HTML for this component. This will execute the component server-side and
4651
/// return the rendered HTML.

src/React.Core/IReactEnvironment.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ public interface IReactEnvironment
8181
/// <param name="props">Props to use</param>
8282
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
8383
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
84+
/// <param name="serverOnly">True if this component only should be rendered server-side. Defaults to false.</param>
8485
/// <returns>The component</returns>
85-
IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false);
86+
IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false, bool serverOnly = false);
8687

8788
/// <summary>
8889
/// Adds the provided <see cref="IReactComponent"/> to the list of components to render client side.

src/React.Core/ReactComponent.cs

+10
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public class ReactComponent : IReactComponent
6868
/// </summary>
6969
public string ContainerClass { get; set; }
7070

71+
/// <summary>
72+
/// Get or sets if this components only should be rendered server side
73+
/// </summary>
74+
public bool ServerOnly { get; set; }
75+
7176
/// <summary>
7277
/// Gets or sets the props for this component
7378
/// </summary>
@@ -130,6 +135,11 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
130135
? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser())
131136
: string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser());
132137
html = _environment.Execute<string>(reactRenderCommand);
138+
139+
if (renderServerOnly)
140+
{
141+
return html;
142+
}
133143
}
134144
catch (JsRuntimeException ex)
135145
{

src/React.Core/ReactEnvironment.cs

+9-6
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
using System.Text;
1515
using System.Threading;
1616
using JavaScriptEngineSwitcher.Core;
17-
using JavaScriptEngineSwitcher.Core.Helpers;
1817
using JSPool;
19-
using Newtonsoft.Json;
2018
using React.Exceptions;
2119
using React.TinyIoC;
2220

@@ -287,8 +285,9 @@ public virtual bool HasVariable(string name)
287285
/// <param name="props">Props to use</param>
288286
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
289287
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
288+
/// <param name="serverOnly">True if this component only should be rendered server-side. Defaults to false.</param>
290289
/// <returns>The component</returns>
291-
public virtual IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false)
290+
public virtual IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false, bool serverOnly = false)
292291
{
293292
if (!clientOnly)
294293
{
@@ -297,7 +296,8 @@ public virtual IReactComponent CreateComponent<T>(string componentName, T props,
297296

298297
var component = new ReactComponent(this, _config, componentName, containerId)
299298
{
300-
Props = props
299+
Props = props,
300+
ServerOnly = serverOnly
301301
};
302302
_components.Add(component);
303303
return component;
@@ -339,8 +339,11 @@ public virtual string GetInitJavaScript(bool clientOnly = false)
339339

340340
foreach (var component in _components)
341341
{
342-
fullScript.Append(component.RenderJavaScript());
343-
fullScript.AppendLine(";");
342+
if (!component.ServerOnly)
343+
{
344+
fullScript.Append(component.RenderJavaScript());
345+
fullScript.AppendLine(";");
346+
}
344347
}
345348

346349
return fullScript.ToString();

tests/React.Tests/Core/ReactComponentTest.cs

+39
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,45 @@ public void RenderHtmlShouldWrapComponentInCustomElement()
124124
Assert.Equal(@"<span id=""container"">[HTML]</span>", result);
125125
}
126126

127+
[Fact]
128+
public void RenderHtmlShouldNotRenderComponentWhenContainerOnly()
129+
{
130+
var config = new Mock<IReactSiteConfiguration>();
131+
config.Setup(x => x.UseServerSideRendering).Returns(true);
132+
var environment = new Mock<IReactEnvironment>();
133+
environment.Setup(x => x.Execute<bool>("typeof Foo !== 'undefined'")).Returns(true);
134+
environment.Setup(x => x.Execute<string>(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))"))
135+
.Returns("[HTML]");
136+
137+
var component = new ReactComponent(environment.Object, config.Object, "Foo", "container")
138+
{
139+
Props = new { hello = "World" },
140+
ContainerTag = "span"
141+
};
142+
var result = component.RenderHtml(true, false);
143+
144+
Assert.Equal(@"<span id=""container""></span>", result);
145+
}
146+
147+
[Fact]
148+
public void RenderHtmlShouldNotWrapComponentWhenServerSideOnly()
149+
{
150+
var config = new Mock<IReactSiteConfiguration>();
151+
config.Setup(x => x.UseServerSideRendering).Returns(true);
152+
var environment = new Mock<IReactEnvironment>();
153+
environment.Setup(x => x.Execute<bool>("typeof Foo !== 'undefined'")).Returns(true);
154+
environment.Setup(x => x.Execute<string>(@"ReactDOMServer.renderToStaticMarkup(React.createElement(Foo, {""hello"":""World""}))"))
155+
.Returns("[HTML]");
156+
157+
var component = new ReactComponent(environment.Object, config.Object, "Foo", "container")
158+
{
159+
Props = new { hello = "World" },
160+
};
161+
var result = component.RenderHtml(false, true);
162+
163+
Assert.Equal(@"[HTML]", result);
164+
}
165+
127166
[Fact]
128167
public void RenderHtmlShouldAddClassToElement()
129168
{

tests/React.Tests/Core/ReactEnvironmentTest.cs

+11
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ public void CreatesIReactComponent()
137137
Assert.Equal(";\r\n", environment.GetInitJavaScript());
138138
}
139139

140+
[Fact]
141+
public void ServerSideOnlyComponentRendersNoJavaScript()
142+
{
143+
var mocks = new Mocks();
144+
var environment = mocks.CreateReactEnvironment();
145+
146+
environment.CreateComponent("HelloWorld", new { name = "Daniel" }, serverOnly: true);
147+
148+
Assert.Equal(string.Empty, environment.GetInitJavaScript());
149+
}
150+
140151
public class Mocks
141152
{
142153
public Mock<PooledJsEngine> Engine { get; private set; }

tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs

+18-13
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
*/
99

1010
using Moq;
11-
using React.Web.Mvc;
1211
using Xunit;
12+
using React.Web.Mvc;
1313

1414
namespace React.Tests.Mvc
1515
{
@@ -36,8 +36,9 @@ public void ReactWithInitShouldReturnHtmlAndScript()
3636
var environment = ConfigureMockEnvironment();
3737
environment.Setup(x => x.CreateComponent(
3838
"ComponentName",
39-
new {},
39+
new { },
4040
null,
41+
false,
4142
false
4243
)).Returns(component.Object);
4344

@@ -63,7 +64,8 @@ public void EngineIsReturnedToPoolAfterRender()
6364
"ComponentName",
6465
new { },
6566
null,
66-
true
67+
true,
68+
false
6769
)).Returns(component.Object);
6870

6971
environment.Verify(x => x.ReturnEngineToPool(), Times.Never);
@@ -73,9 +75,9 @@ public void EngineIsReturnedToPoolAfterRender()
7375
props: new { },
7476
htmlTag: "span",
7577
clientOnly: true,
76-
serverOnly: true
78+
serverOnly: false
7779
);
78-
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
80+
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
7981
environment.Verify(x => x.ReturnEngineToPool(), Times.Once);
8082
}
8183

@@ -87,9 +89,10 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue()
8789
var environment = ConfigureMockEnvironment();
8890
environment.Setup(x => x.CreateComponent(
8991
"ComponentName",
90-
new {},
92+
new { },
9193
null,
92-
true
94+
true,
95+
false
9396
)).Returns(component.Object);
9497

9598
var result = HtmlHelperExtensions.React(
@@ -98,20 +101,22 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue()
98101
props: new { },
99102
htmlTag: "span",
100103
clientOnly: true,
101-
serverOnly: true
104+
serverOnly: false
102105
);
103-
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
106+
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
104107
}
105108

106109
[Fact]
107-
public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() {
110+
public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue()
111+
{
108112
var component = new Mock<IReactComponent>();
109-
component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML");
113+
component.Setup(x => x.RenderHtml(false, true, null)).Returns("HTML");
110114
var environment = ConfigureMockEnvironment();
111115
environment.Setup(x => x.CreateComponent(
112116
"ComponentName",
113117
new { },
114118
null,
119+
false,
115120
true
116121
)).Returns(component.Object);
117122

@@ -120,10 +125,10 @@ public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() {
120125
componentName: "ComponentName",
121126
props: new { },
122127
htmlTag: "span",
123-
clientOnly: true,
128+
clientOnly: false,
124129
serverOnly: true
125130
);
126-
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
131+
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == false), It.Is<bool>(z => z == true), null), Times.Once);
127132
}
128133
}
129134
}

0 commit comments

Comments
 (0)