Skip to content

Improve server side only rendering #497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
6 changes: 3 additions & 3 deletions src/React.AspNet/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand Down Expand Up @@ -54,7 +54,7 @@ private static IReactEnvironment Environment
/// <param name="htmlTag">HTML tag to wrap the component in. Defaults to &lt;div&gt;</param>
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
/// <param name="clientOnly">Skip rendering server-side and only output client-side initialisation code. Defaults to <c>false</c></param>
/// <param name="serverOnly">Skip rendering React specific data-attributes during server side rendering. Defaults to <c>false</c></param>
/// <param name="serverOnly">Skip rendering React specific data-attributes, container and client-side initialisation during server side rendering. Defaults to <c>false</c></param>
/// <param name="containerClass">HTML class(es) to set on the container tag</param>
/// <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>
/// <returns>The component's HTML</returns>
Expand All @@ -72,7 +72,7 @@ public static IHtmlString React<T>(
{
try
{
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly);
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly, serverOnly);
if (!string.IsNullOrEmpty(htmlTag))
{
reactComponent.ContainerTag = htmlTag;
Expand Down
5 changes: 5 additions & 0 deletions src/React.Core/IReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public interface IReactComponent
/// </summary>
string ContainerClass { get; set; }

/// <summary>
/// Get or sets if this components only should be rendered server side
/// </summary>
bool ServerOnly { get; set; }

/// <summary>
/// Renders the HTML for this component. This will execute the component server-side and
/// return the rendered HTML.
Expand Down
5 changes: 3 additions & 2 deletions src/React.Core/IReactEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand Down Expand Up @@ -81,8 +81,9 @@ public interface IReactEnvironment
/// <param name="props">Props to use</param>
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
/// <param name="serverOnly">True if this component only should be rendered server-side. Defaults to false.</param>
/// <returns>The component</returns>
IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false);
IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false, bool serverOnly = false);

/// <summary>
/// Adds the provided <see cref="IReactComponent"/> to the list of components to render client side.
Expand Down
10 changes: 10 additions & 0 deletions src/React.Core/ReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public class ReactComponent : IReactComponent
/// </summary>
public string ContainerClass { get; set; }

/// <summary>
/// Get or sets if this components only should be rendered server side
/// </summary>
public bool ServerOnly { get; set; }

/// <summary>
/// Gets or sets the props for this component
/// </summary>
Expand Down Expand Up @@ -130,6 +135,11 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser())
: string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser());
html = _environment.Execute<string>(reactRenderCommand);

if (renderServerOnly)
{
return html;
}
}
catch (JsRuntimeException ex)
{
Expand Down
15 changes: 10 additions & 5 deletions src/React.Core/ReactEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand Down Expand Up @@ -287,8 +287,9 @@ public virtual bool HasVariable(string name)
/// <param name="props">Props to use</param>
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
/// <param name="serverOnly">True if this component only should be rendered server-side. Defaults to false.</param>
/// <returns>The component</returns>
public virtual IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false)
public virtual IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false, bool serverOnly = false)
{
if (!clientOnly)
{
Expand All @@ -297,7 +298,8 @@ public virtual IReactComponent CreateComponent<T>(string componentName, T props,

var component = new ReactComponent(this, _config, componentName, containerId)
{
Props = props
Props = props,
ServerOnly = serverOnly
};
_components.Add(component);
return component;
Expand Down Expand Up @@ -339,8 +341,11 @@ public virtual string GetInitJavaScript(bool clientOnly = false)

foreach (var component in _components)
{
fullScript.Append(component.RenderJavaScript());
fullScript.AppendLine(";");
if(!component.ServerOnly)
{
fullScript.Append(component.RenderJavaScript());
fullScript.AppendLine(";");
}
}

return fullScript.ToString();
Expand Down
39 changes: 39 additions & 0 deletions tests/React.Tests/Core/ReactComponentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,45 @@ public void RenderHtmlShouldWrapComponentInCustomElement()
Assert.Equal(@"<span id=""container"">[HTML]</span>", result);
}

[Fact]
public void RenderHtmlShouldNotRenderComponentWhenContainerOnly()
{
var config = new Mock<IReactSiteConfiguration>();
config.Setup(x => x.UseServerSideRendering).Returns(true);
var environment = new Mock<IReactEnvironment>();
environment.Setup(x => x.Execute<bool>("typeof Foo !== 'undefined'")).Returns(true);
environment.Setup(x => x.Execute<string>(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))"))
.Returns("[HTML]");

var component = new ReactComponent(environment.Object, config.Object, "Foo", "container")
{
Props = new { hello = "World" },
ContainerTag = "span"
};
var result = component.RenderHtml(true, false);

Assert.Equal(@"<span id=""container""></span>", result);
}

[Fact]
public void RenderHtmlShouldNotWrapComponentWhenServerSideOnly()
{
var config = new Mock<IReactSiteConfiguration>();
config.Setup(x => x.UseServerSideRendering).Returns(true);
var environment = new Mock<IReactEnvironment>();
environment.Setup(x => x.Execute<bool>("typeof Foo !== 'undefined'")).Returns(true);
environment.Setup(x => x.Execute<string>(@"ReactDOMServer.renderToStaticMarkup(React.createElement(Foo, {""hello"":""World""}))"))
.Returns("[HTML]");

var component = new ReactComponent(environment.Object, config.Object, "Foo", "container")
{
Props = new { hello = "World" },
};
var result = component.RenderHtml(false, true);

Assert.Equal(@"[HTML]", result);
}

[Fact]
public void RenderHtmlShouldAddClassToElement()
{
Expand Down
13 changes: 12 additions & 1 deletion tests/React.Tests/Core/ReactEnvironmentTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand Down Expand Up @@ -137,6 +137,17 @@ public void CreatesIReactComponent()
Assert.Equal(";\r\n", environment.GetInitJavaScript());
}

[Fact]
public void ServerSideOnlyComponentRendersNoJavaScript()
{
var mocks = new Mocks();
var environment = mocks.CreateReactEnvironment();

environment.CreateComponent("HelloWorld", new { name = "Daniel" }, serverOnly: true);

Assert.Equal(string.Empty, environment.GetInitJavaScript());
}

public class Mocks
{
public Mock<PooledJsEngine> Engine { get; private set; }
Expand Down
35 changes: 20 additions & 15 deletions tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
*/

using Moq;
using React.Web.Mvc;
using Xunit;
using React.Web.Mvc;

namespace React.Tests.Mvc
{
public class HtmlHelperExtensionsTests
{
{
/// <summary>
/// Creates a mock <see cref="IReactEnvironment"/> and registers it with the IoC container
/// This is only required because <see cref="HtmlHelperExtensions"/> can not be
Expand All @@ -36,8 +36,9 @@ public void ReactWithInitShouldReturnHtmlAndScript()
var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
"ComponentName",
new {},
new { },
null,
false,
false
)).Returns(component.Object);

Expand All @@ -63,7 +64,8 @@ public void EngineIsReturnedToPoolAfterRender()
"ComponentName",
new { },
null,
true
true,
false
)).Returns(component.Object);

environment.Verify(x => x.ReturnEngineToPool(), Times.Never);
Expand All @@ -73,9 +75,9 @@ public void EngineIsReturnedToPoolAfterRender()
props: new { },
htmlTag: "span",
clientOnly: true,
serverOnly: true
serverOnly: false
);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
environment.Verify(x => x.ReturnEngineToPool(), Times.Once);
}

Expand All @@ -87,9 +89,10 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue()
var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
"ComponentName",
new {},
new { },
null,
true
true,
false
)).Returns(component.Object);

var result = HtmlHelperExtensions.React(
Expand All @@ -98,20 +101,22 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue()
props: new { },
htmlTag: "span",
clientOnly: true,
serverOnly: true
serverOnly: false
);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
}

[Fact]
public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() {
public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue()
{
var component = new Mock<IReactComponent>();
component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML");
component.Setup(x => x.RenderHtml(false, true, null)).Returns("HTML");
var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
"ComponentName",
new { },
null,
false,
true
)).Returns(component.Object);

Expand All @@ -120,10 +125,10 @@ public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() {
componentName: "ComponentName",
props: new { },
htmlTag: "span",
clientOnly: true,
clientOnly: false,
serverOnly: true
);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == false), It.Is<bool>(z => z == true), null), Times.Once);
}
}
}
}