diff --git a/src/React.AspNet/ActionHtmlString.cs b/src/React.AspNet/ActionHtmlString.cs
index a2879dde2..99179e9b5 100644
--- a/src/React.AspNet/ActionHtmlString.cs
+++ b/src/React.AspNet/ActionHtmlString.cs
@@ -11,6 +11,7 @@
using System.IO;
#if LEGACYASPNET
+using System.Text;
using System.Web;
#else
using System.Text.Encodings.Web;
@@ -40,13 +41,26 @@ public ActionHtmlString(Action textWriter)
}
#if LEGACYASPNET
+ [ThreadStatic]
+ private static StringWriter _sharedStringWriter;
+
/// Returns an HTML-encoded string.
/// An HTML-encoded string.
public string ToHtmlString()
{
- var sw = new StringWriter();
- _textWriter(sw);
- return sw.ToString();
+ var stringWriter = _sharedStringWriter;
+ if (stringWriter != null)
+ {
+ stringWriter.GetStringBuilder().Clear();
+ }
+ else
+ {
+ _sharedStringWriter = stringWriter = new StringWriter(new StringBuilder(128));
+ }
+
+ _textWriter(stringWriter);
+
+ return stringWriter.ToString();
}
#else
///
diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs
index cd821cd48..acc88f11f 100644
--- a/src/React.AspNet/HtmlHelperExtensions.cs
+++ b/src/React.AspNet/HtmlHelperExtensions.cs
@@ -81,7 +81,7 @@ public static IHtmlString React(
reactComponent.ContainerClass = containerClass;
}
- writer.Write(reactComponent.RenderHtml(clientOnly, serverOnly, exceptionHandler));
+ reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler);
}
finally
{
@@ -131,9 +131,9 @@ public static IHtmlString ReactWithInit(
reactComponent.ContainerClass = containerClass;
}
- writer.Write(reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler));
+ reactComponent.RenderHtml(writer, clientOnly, exceptionHandler: exceptionHandler);
writer.WriteLine();
- WriteScriptTag(writer, bodyWriter => bodyWriter.Write(reactComponent.RenderJavaScript()));
+ WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter));
}
finally
{
@@ -153,7 +153,7 @@ public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool
{
try
{
- WriteScriptTag(writer, bodyWriter => bodyWriter.Write(Environment.GetInitJavaScript(clientOnly)));
+ WriteScriptTag(writer, bodyWriter => Environment.GetInitJavaScript(bodyWriter, clientOnly));
}
finally
{
diff --git a/src/React.Core/IReactComponent.cs b/src/React.Core/IReactComponent.cs
index b4a6d4428..45ac47ffd 100644
--- a/src/React.Core/IReactComponent.cs
+++ b/src/React.Core/IReactComponent.cs
@@ -8,6 +8,7 @@
*/
using System;
+using System.IO;
namespace React
{
@@ -63,5 +64,24 @@ public interface IReactComponent
///
/// JavaScript
string RenderJavaScript();
+
+ ///
+ /// Renders the HTML for this component. This will execute the component server-side and
+ /// return the rendered HTML.
+ ///
+ /// The to which the content is written
+ /// Only renders component container. Used for client-side only rendering.
+ /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.
+ /// A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)
+ /// HTML
+ void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action exceptionHandler = null);
+
+ ///
+ /// Renders the JavaScript required to initialise this component client-side. This will
+ /// initialise the React component, which includes attach event handlers to the
+ /// server-rendered HTML.
+ ///
+ /// JavaScript
+ void RenderJavaScript(TextWriter writer);
}
}
diff --git a/src/React.Core/IReactEnvironment.cs b/src/React.Core/IReactEnvironment.cs
index 67991360f..4ffe32630 100644
--- a/src/React.Core/IReactEnvironment.cs
+++ b/src/React.Core/IReactEnvironment.cs
@@ -8,6 +8,8 @@
*/
+using System.IO;
+
namespace React
{
///
@@ -114,5 +116,14 @@ public interface IReactEnvironment
/// Gets the site-wide configuration.
///
IReactSiteConfiguration Configuration { get; }
+
+ ///
+ /// Renders the JavaScript required to initialise all components client-side. This will
+ /// attach event handlers to the server-rendered HTML.
+ ///
+ /// The to which the content is written
+ /// True if server-side rendering will be bypassed. Defaults to false.
+ /// JavaScript for all components
+ void GetInitJavaScript(TextWriter writer, bool clientOnly = false);
}
}
diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs
index 511b97531..376315779 100644
--- a/src/React.Core/ReactComponent.cs
+++ b/src/React.Core/ReactComponent.cs
@@ -9,7 +9,9 @@
using System;
using System.Collections.Concurrent;
+using System.IO;
using System.Linq;
+using System.Text;
using System.Text.RegularExpressions;
using JavaScriptEngineSwitcher.Core;
using Newtonsoft.Json;
@@ -24,6 +26,9 @@ public class ReactComponent : IReactComponent
{
private static readonly ConcurrentDictionary _componentNameValidCache = new ConcurrentDictionary(StringComparer.Ordinal);
+ [ThreadStatic]
+ private static StringWriter _sharedStringWriter;
+
///
/// Regular expression used to validate JavaScript identifiers. Used to ensure component
/// names are valid.
@@ -87,8 +92,7 @@ public object Props
_props = value;
_serializedProps = JsonConvert.SerializeObject(
value,
- _configuration.JsonSerializerSettings
- );
+ _configuration.JsonSerializerSettings);
}
}
@@ -119,6 +123,24 @@ public ReactComponent(IReactEnvironment environment, IReactSiteConfiguration con
/// A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)
/// HTML
public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action exceptionHandler = null)
+ {
+ using (var writer = new StringWriter())
+ {
+ RenderHtml(writer, renderContainerOnly, renderServerOnly, exceptionHandler);
+ return writer.ToString();
+ }
+ }
+
+ ///
+ /// Renders the HTML for this component. This will execute the component server-side and
+ /// return the rendered HTML.
+ ///
+ /// The to which the content is written
+ /// Only renders component container. Used for client-side only rendering.
+ /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.
+ /// A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)
+ /// HTML
+ public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action exceptionHandler = null)
{
if (!_configuration.UseServerSideRendering)
{
@@ -133,16 +155,28 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
var html = string.Empty;
if (!renderContainerOnly)
{
+ var stringWriter = _sharedStringWriter;
+ if (stringWriter != null)
+ {
+ stringWriter.GetStringBuilder().Clear();
+ }
+ else
+ {
+ _sharedStringWriter = stringWriter = new StringWriter(new StringBuilder(_serializedProps.Length + 128));
+ }
+
try
{
- var reactRenderCommand = renderServerOnly
- ? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser())
- : string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser());
- html = _environment.Execute(reactRenderCommand);
+ stringWriter.Write(renderServerOnly ? "ReactDOMServer.renderToStaticMarkup(" : "ReactDOMServer.renderToString(");
+ WriteComponentInitialiser(stringWriter);
+ stringWriter.Write(')');
+
+ html = _environment.Execute(stringWriter.ToString());
if (renderServerOnly)
{
- return html;
+ writer.Write(html);
+ return;
}
}
catch (JsRuntimeException ex)
@@ -156,18 +190,23 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
}
}
- string attributes = string.Format("id=\"{0}\"", ContainerId);
+ writer.Write('<');
+ writer.Write(ContainerTag);
+ writer.Write(" id=\"");
+ writer.Write(ContainerId);
+ writer.Write('"');
if (!string.IsNullOrEmpty(ContainerClass))
{
- attributes += string.Format(" class=\"{0}\"", ContainerClass);
+ writer.Write(" class=\"");
+ writer.Write(ContainerClass);
+ writer.Write('"');
}
- return string.Format(
- "<{2} {0}>{1}{2}>",
- attributes,
- html,
- ContainerTag
- );
+ writer.Write('>');
+ writer.Write(html);
+ writer.Write("");
+ writer.Write(ContainerTag);
+ writer.Write('>');
}
///
@@ -178,11 +217,27 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
/// JavaScript
public virtual string RenderJavaScript()
{
- return string.Format(
- "ReactDOM.hydrate({0}, document.getElementById({1}))",
- GetComponentInitialiser(),
- JsonConvert.SerializeObject(ContainerId, _configuration.JsonSerializerSettings) // SerializeObject accepts null settings
- );
+ using (var writer = new StringWriter())
+ {
+ RenderJavaScript(writer);
+ return writer.ToString();
+ }
+ }
+
+ ///
+ /// Renders the JavaScript required to initialise this component client-side. This will
+ /// initialise the React component, which includes attach event handlers to the
+ /// server-rendered HTML.
+ ///
+ /// The to which the content is written
+ /// JavaScript
+ public virtual void RenderJavaScript(TextWriter writer)
+ {
+ writer.Write("ReactDOM.hydrate(");
+ WriteComponentInitialiser(writer);
+ writer.Write(", document.getElementById(\"");
+ writer.Write(ContainerId);
+ writer.Write("\"))");
}
///
@@ -208,14 +263,14 @@ protected virtual void EnsureComponentExists()
///
/// Gets the JavaScript code to initialise the component
///
- /// JavaScript for component initialisation
- protected virtual string GetComponentInitialiser()
+ /// The to which the content is written
+ protected virtual void WriteComponentInitialiser(TextWriter writer)
{
- return string.Format(
- "React.createElement({0}, {1})",
- ComponentName,
- _serializedProps
- );
+ writer.Write("React.createElement(");
+ writer.Write(ComponentName);
+ writer.Write(", ");
+ writer.Write(_serializedProps);
+ writer.Write(')');
}
///
diff --git a/src/React.Core/ReactEnvironment.cs b/src/React.Core/ReactEnvironment.cs
index edecc5e9d..1f9ebec9f 100644
--- a/src/React.Core/ReactEnvironment.cs
+++ b/src/React.Core/ReactEnvironment.cs
@@ -10,6 +10,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
@@ -335,25 +336,37 @@ public virtual IReactComponent CreateComponent(IReactComponent component, bool c
/// JavaScript for all components
public virtual string GetInitJavaScript(bool clientOnly = false)
{
- var fullScript = new StringBuilder();
-
+ using (var writer = new StringWriter())
+ {
+ GetInitJavaScript(writer, clientOnly);
+ return writer.ToString();
+ }
+ }
+
+ ///
+ /// Renders the JavaScript required to initialise all components client-side. This will
+ /// attach event handlers to the server-rendered HTML.
+ ///
+ /// The to which the content is written
+ /// True if server-side rendering will be bypassed. Defaults to false.
+ /// JavaScript for all components
+ public virtual void GetInitJavaScript(TextWriter writer, bool clientOnly = false)
+ {
// Propagate any server-side console.log calls to corresponding client-side calls.
if (!clientOnly)
{
var consoleCalls = Execute("console.getCalls()");
- fullScript.Append(consoleCalls);
+ writer.Write(consoleCalls);
}
-
+
foreach (var component in _components)
{
if (!component.ServerOnly)
{
- fullScript.Append(component.RenderJavaScript());
- fullScript.AppendLine(";");
+ component.RenderJavaScript(writer);
+ writer.WriteLine(';');
}
}
-
- return fullScript.ToString();
}
///
diff --git a/src/React.Router/ReactRouterComponent.cs b/src/React.Router/ReactRouterComponent.cs
index 993ac2953..631b2cb50 100644
--- a/src/React.Router/ReactRouterComponent.cs
+++ b/src/React.Router/ReactRouterComponent.cs
@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
+using System.IO;
using Newtonsoft.Json;
namespace React.Router
@@ -67,15 +68,15 @@ public virtual ExecutionResult RenderRouterWithContext(bool renderContainerOnly
/// Gets the JavaScript code to initialise the component
///
/// JavaScript for component initialisation
- protected override string GetComponentInitialiser()
+ protected override void WriteComponentInitialiser(TextWriter writer)
{
- return string.Format(
- @"React.createElement
- ({0}, Object.assign({1}, {{ location: '{2}', context: context }}))",
- ComponentName,
- _serializedProps,
- _path
- );
+ writer.Write("React.createElement(");
+ writer.Write(ComponentName);
+ writer.Write(", Object.assign(");
+ writer.Write(_serializedProps);
+ writer.Write(", { location: '");
+ writer.Write(_path);
+ writer.Write("', context: context }))");
}
///
@@ -86,13 +87,13 @@ protected override string GetComponentInitialiser()
/// Client side React Router does not need context nor explicit path parameter.
///
/// JavaScript
- public override string RenderJavaScript()
+ public override void RenderJavaScript(TextWriter writer)
{
- return string.Format(
- "ReactDOM.hydrate({0}, document.getElementById({1}))",
- base.GetComponentInitialiser(),
- JsonConvert.SerializeObject(ContainerId, _configuration.JsonSerializerSettings) // SerializeObject accepts null settings
- );
+ writer.Write("ReactDOM.hydrate(");
+ base.WriteComponentInitialiser(writer);
+ writer.Write(", document.getElementById(\"");
+ writer.Write(ContainerId);
+ writer.Write("\"))");
}
}
}
diff --git a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
index f2a41735f..53c6b594e 100644
--- a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
+++ b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
@@ -8,6 +8,7 @@
*/
using System;
+using System.IO;
using System.Security.Cryptography;
using Moq;
using React.Web.Mvc;
@@ -34,8 +35,12 @@ private Mock ConfigureMockEnvironment(IReactSiteConfiguration
public void ReactWithInitShouldReturnHtmlAndScript()
{
var component = new Mock();
- component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML");
- component.Setup(x => x.RenderJavaScript()).Returns("JS");
+
+ component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null))
+ .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler) => writer.Write("HTML"));
+
+ component.Setup(x => x.RenderJavaScript(It.IsAny())).Callback((TextWriter writer) => writer.Write("JS"));
+
var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
"ComponentName",
@@ -51,6 +56,7 @@ public void ReactWithInitShouldReturnHtmlAndScript()
props: new { },
htmlTag: "span"
).ToHtmlString();
+
Assert.Equal(
"HTML" + System.Environment.NewLine + "",
result.ToString()
@@ -69,8 +75,10 @@ public void ScriptNonceIsReturned()
}
var component = new Mock();
- component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML");
- component.Setup(x => x.RenderJavaScript()).Returns("JS");
+ component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null))
+ .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler) => writer.Write("HTML")).Verifiable();
+
+ component.Setup(x => x.RenderJavaScript(It.IsAny())).Callback((TextWriter writer) => writer.Write("JS")).Verifiable();
var config = new Mock();
@@ -92,6 +100,7 @@ public void ScriptNonceIsReturned()
props: new { },
htmlTag: "span"
).ToHtmlString();
+
Assert.Equal(
"HTML" + System.Environment.NewLine + "",
result.ToString()
@@ -106,6 +115,7 @@ public void ScriptNonceIsReturned()
props: new { },
htmlTag: "span"
).ToHtmlString();
+
Assert.Equal(
"HTML" + System.Environment.NewLine + "",
result.ToString()
@@ -116,7 +126,9 @@ public void ScriptNonceIsReturned()
public void EngineIsReturnedToPoolAfterRender()
{
var component = new Mock();
- component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML");
+ component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null))
+ .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler) => writer.Write("HTML")).Verifiable();
+
var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
"ComponentName",
@@ -135,7 +147,8 @@ public void EngineIsReturnedToPoolAfterRender()
clientOnly: true,
serverOnly: false
).ToHtmlString();
- component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == false), null), Times.Once);
+
+ component.Verify(x => x.RenderHtml(It.IsAny(), It.Is(y => y == true), It.Is(z => z == false), null), Times.Once);
environment.Verify(x => x.ReturnEngineToPool(), Times.Once);
}
@@ -143,7 +156,9 @@ public void EngineIsReturnedToPoolAfterRender()
public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue()
{
var component = new Mock();
- component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML");
+ component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null))
+ .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler) => writer.Write("HTML")).Verifiable();
+
var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
"ComponentName",
@@ -161,14 +176,17 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue()
clientOnly: true,
serverOnly: false
).ToHtmlString();
- component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == false), null), Times.Once);
+
+ component.Verify(x => x.RenderHtml(It.IsAny(), It.Is(y => y == true), It.Is(z => z == false), null), Times.Once);
}
[Fact]
public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue()
{
var component = new Mock();
- component.Setup(x => x.RenderHtml(false, true, null)).Returns("HTML");
+ component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null))
+ .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler) => writer.Write("HTML")).Verifiable();
+
var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
"ComponentName",
@@ -186,7 +204,8 @@ public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue()
clientOnly: false,
serverOnly: true
).ToHtmlString();
- component.Verify(x => x.RenderHtml(It.Is(y => y == false), It.Is(z => z == true), null), Times.Once);
+
+ component.Verify(x => x.RenderHtml(It.IsAny(), It.Is(y => y == false), It.Is(z => z == true), null), Times.Once);
}
}
}