From 9fcc9eab520b89019be21b30b04b27ef06394242 Mon Sep 17 00:00:00 2001
From: Daniil Sokolyuk <d.sokolyuk@rambler-co.ru>
Date: Thu, 12 Apr 2018 17:02:05 +0300
Subject: [PATCH 1/2] Text writer implementation

---
 src/React.AspNet/ActionHtmlString.cs          |  20 +++-
 src/React.AspNet/HtmlHelperExtensions.cs      |   8 +-
 src/React.Core/IReactComponent.cs             |  20 ++++
 src/React.Core/IReactEnvironment.cs           |  11 ++
 src/React.Core/ReactComponent.cs              | 109 +++++++++++++-----
 src/React.Core/ReactEnvironment.cs            |  29 +++--
 src/React.Router/ReactRouterComponent.cs      |  29 ++---
 .../Mvc/HtmlHelperExtensionsTests.cs          |  39 +++++--
 8 files changed, 199 insertions(+), 66 deletions(-)

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> textWriter)
 		}
 
 #if LEGACYASPNET
+		[ThreadStatic]
+		private static StringWriter _sharedStringWriter;
+
 		/// <summary>Returns an HTML-encoded string.</summary>
 		/// <returns>An HTML-encoded string.</returns>
 		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
 		/// <summary>
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<T>(
 						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<T>(
 						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
 		/// </summary>
 		/// <returns>JavaScript</returns>
 		string RenderJavaScript();
+
+		/// <summary>
+		/// Renders the HTML for this component. This will execute the component server-side and
+		/// return the rendered HTML.
+		/// </summary>
+		/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
+		/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
+		/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</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>HTML</returns>
+		void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null);
+
+		/// <summary>
+		/// 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.
+		/// </summary>
+		/// <returns>JavaScript</returns>
+		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
 {
 	/// <summary>
@@ -114,5 +116,14 @@ public interface IReactEnvironment
 		/// Gets the site-wide configuration.
 		/// </summary>
 		IReactSiteConfiguration Configuration { get; }
+
+		/// <summary>
+		/// Renders the JavaScript required to initialise all components client-side. This will 
+		/// attach event handlers to the server-rendered HTML.
+		/// </summary>
+		/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
+		/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
+		/// <returns>JavaScript for all components</returns>
+		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<string, bool> _componentNameValidCache = new ConcurrentDictionary<string, bool>(StringComparer.Ordinal);
 
+		[ThreadStatic]
+		private static StringWriter _sharedStringWriter;
+
 		/// <summary>
 		/// 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
 		/// <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>HTML</returns>
 		public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
+		{
+			using (var writer = new StringWriter())
+			{
+				RenderHtml(writer, renderContainerOnly, renderServerOnly, exceptionHandler);
+				return writer.ToString();
+			}
+		}
+
+		/// <summary>
+		/// Renders the HTML for this component. This will execute the component server-side and
+		/// return the rendered HTML.
+		/// </summary>
+		/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
+		/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
+		/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</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>HTML</returns>
+		public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> 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<string>(reactRenderCommand);
+					stringWriter.Write(renderServerOnly ? "ReactDOMServer.renderToStaticMarkup(" : "ReactDOMServer.renderToString(");
+					WriteComponentInitialiser(stringWriter);
+					stringWriter.Write(')');
+
+					html = _environment.Execute<string>(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('>');
 		}
 
 		/// <summary>
@@ -178,11 +217,27 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
 		/// <returns>JavaScript</returns>
 		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();
+			}
+		}
+
+		/// <summary>
+		/// 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.
+		/// </summary>
+		/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
+		/// <returns>JavaScript</returns>
+		public virtual void RenderJavaScript(TextWriter writer)
+		{
+			writer.Write("ReactDOM.hydrate(");
+			WriteComponentInitialiser(writer);
+			writer.Write(", document.getElementById(\"");
+			writer.Write(ContainerId);
+			writer.Write("\"))");
 		}
 
 		/// <summary>
@@ -208,14 +263,14 @@ protected virtual void EnsureComponentExists()
 		/// <summary>
 		/// Gets the JavaScript code to initialise the component
 		/// </summary>
-		/// <returns>JavaScript for component initialisation</returns>
-		protected virtual string GetComponentInitialiser()
+		/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
+		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(')');
 		}
 
 		/// <summary>
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
 		/// <returns>JavaScript for all components</returns>
 		public virtual string GetInitJavaScript(bool clientOnly = false)
 		{
-			var fullScript = new StringBuilder();
-			
+			using (var writer = new StringWriter())
+			{
+				GetInitJavaScript(writer, clientOnly);
+				return writer.ToString();
+			}
+		}
+
+		/// <summary>
+		/// Renders the JavaScript required to initialise all components client-side. This will 
+		/// attach event handlers to the server-rendered HTML.
+		/// </summary>
+		/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
+		/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
+		/// <returns>JavaScript for all components</returns>
+		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<string>("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();
 		}
 
 		/// <summary>
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
 		/// </summary>
 		/// <returns>JavaScript for component initialisation</returns>
-		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 }))");
 		}
 
 		/// <summary>
@@ -86,13 +87,13 @@ protected override string GetComponentInitialiser()
 		/// Client side React Router does not need context nor explicit path parameter.
 		/// </summary>
 		/// <returns>JavaScript</returns>
-		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..ae7afa5d6 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<IReactEnvironment> ConfigureMockEnvironment(IReactSiteConfiguration
 		public void ReactWithInitShouldReturnHtmlAndScript()
 		{
 			var component = new Mock<IReactComponent>();
-			component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML");
-			component.Setup(x => x.RenderJavaScript()).Returns("JS");
+
+			component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null))
+				.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler) => writer.Write("HTML")).Verifiable();
+
+			component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>())).Callback((TextWriter writer) => writer.Write("JS")).Verifiable();
+
 			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 + "<script>JS</script>",
 				result.ToString()
@@ -69,8 +75,10 @@ public void ScriptNonceIsReturned()
 			}
 
 			var component = new Mock<IReactComponent>();
-			component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML");
-			component.Setup(x => x.RenderJavaScript()).Returns("JS");
+			component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null))
+				.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler) => writer.Write("HTML")).Verifiable();
+
+			component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>())).Callback((TextWriter writer) => writer.Write("JS")).Verifiable();
 
 			var config = new Mock<IReactSiteConfiguration>();
 
@@ -92,6 +100,7 @@ public void ScriptNonceIsReturned()
 				props: new { },
 				htmlTag: "span"
 			).ToHtmlString();
+
 			Assert.Equal(
 				"HTML" + System.Environment.NewLine + "<script>JS</script>",
 				result.ToString()
@@ -106,6 +115,7 @@ public void ScriptNonceIsReturned()
 				props: new { },
 				htmlTag: "span"
 			).ToHtmlString();
+
 			Assert.Equal(
 				"HTML" + System.Environment.NewLine + "<script nonce=\"" + nonce + "\">JS</script>",
 				result.ToString()
@@ -116,7 +126,9 @@ public void ScriptNonceIsReturned()
 		public void EngineIsReturnedToPoolAfterRender()
 		{
 			var component = new Mock<IReactComponent>();
-			component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML");
+			component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null))
+				.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> 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<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
+
+			component.Verify(x => x.RenderHtml(It.IsAny<TextWriter>(), It.Is<bool>(y => y == true), It.Is<bool>(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<IReactComponent>();
-			component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML");
+			component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null))
+				.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> 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<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
+
+			component.Verify(x => x.RenderHtml(It.IsAny<TextWriter>(), It.Is<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
 		}
 
 		[Fact]
 		public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue()
 		{
 			var component = new Mock<IReactComponent>();
-			component.Setup(x => x.RenderHtml(false, true, null)).Returns("HTML");
+			component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null))
+				.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> 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<bool>(y => y == false), It.Is<bool>(z => z == true), null), Times.Once);
+
+			component.Verify(x => x.RenderHtml(It.IsAny<TextWriter>(), It.Is<bool>(y => y == false), It.Is<bool>(z => z == true), null), Times.Once);
 		}
 	}
 }

From 5b53a3cb83b36b7ca221a99876213707ce7789e0 Mon Sep 17 00:00:00 2001
From: Dustin Masters <ceo@dustinsoftware.com>
Date: Wed, 25 Apr 2018 21:16:15 -0700
Subject: [PATCH 2/2] Remove redundant verifiable calls

---
 tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
index ae7afa5d6..53c6b594e 100644
--- a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
+++ b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
@@ -37,9 +37,9 @@ public void ReactWithInitShouldReturnHtmlAndScript()
 			var component = new Mock<IReactComponent>();
 
 			component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null))
-				.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler) => writer.Write("HTML")).Verifiable();
+				.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler) => writer.Write("HTML"));
 
-			component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>())).Callback((TextWriter writer) => writer.Write("JS")).Verifiable();
+			component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>())).Callback((TextWriter writer) => writer.Write("JS"));
 
 			var environment = ConfigureMockEnvironment();
 			environment.Setup(x => x.CreateComponent(