From 72d95ae88dbd61fa94bea64ae4fd5ce9d8e8b69b Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Sun, 12 Nov 2017 15:24:38 -0800 Subject: [PATCH 1/5] Add support for exception handling during component render --- src/React.Core/IReactSiteConfiguration.cs | 6 +++ src/React.Core/ReactComponent.cs | 45 +++++++++-------- src/React.Core/ReactSiteConfiguration.cs | 7 +++ .../Controllers/HomeController.cs | 4 +- .../Views/Home/Index.cshtml | 2 +- .../wwwroot/js/Sample.jsx | 49 ++++++++++++++++++- 6 files changed, 89 insertions(+), 24 deletions(-) diff --git a/src/React.Core/IReactSiteConfiguration.cs b/src/React.Core/IReactSiteConfiguration.cs index bd10fee0f..6785d1705 100644 --- a/src/React.Core/IReactSiteConfiguration.cs +++ b/src/React.Core/IReactSiteConfiguration.cs @@ -179,5 +179,11 @@ public interface IReactSiteConfiguration /// Disables server-side rendering. This is useful when debugging your scripts. /// IReactSiteConfiguration DisableServerSideRendering(); + + /// + /// Handle an exception caught during server-render of a component. + /// If unset, unhandled exceptions will be thrown for all component renders. + /// + Action HandleRenderException { get; set; } } } diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs index 43c40628c..7e9e86bb1 100644 --- a/src/React.Core/ReactComponent.cs +++ b/src/React.Core/ReactComponent.cs @@ -120,39 +120,42 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe EnsureComponentExists(); } - try + var html = string.Empty; + if (!renderContainerOnly) { - var html = string.Empty; - if (!renderContainerOnly) + try { var reactRenderCommand = renderServerOnly ? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser()) : string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser()); html = _environment.Execute(reactRenderCommand); } - - string attributes = string.Format("id=\"{0}\"", ContainerId); - if (!string.IsNullOrEmpty(ContainerClass)) + catch (JsRuntimeException ex) { - attributes += string.Format(" class=\"{0}\"", ContainerClass); + if (_configuration.HandleRenderException == null) { + throw new ReactServerRenderingException(string.Format( + "Error while rendering \"{0}\" to \"{2}\": {1}", + ComponentName, + ex.Message, + ContainerId + )); + } + _configuration.HandleRenderException(ex); } - - return string.Format( - "<{2} {0}>{1}", - attributes, - html, - ContainerTag - ); } - catch (JsRuntimeException ex) + + string attributes = string.Format("id=\"{0}\"", ContainerId); + if (!string.IsNullOrEmpty(ContainerClass)) { - throw new ReactServerRenderingException(string.Format( - "Error while rendering \"{0}\" to \"{2}\": {1}", - ComponentName, - ex.Message, - ContainerId - )); + attributes += string.Format(" class=\"{0}\"", ContainerClass); } + + return string.Format( + "<{2} {0}>{1}", + attributes, + html, + ContainerTag + ); } /// diff --git a/src/React.Core/ReactSiteConfiguration.cs b/src/React.Core/ReactSiteConfiguration.cs index bab2e5e23..ef10a855a 100644 --- a/src/React.Core/ReactSiteConfiguration.cs +++ b/src/React.Core/ReactSiteConfiguration.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using System.Collections.Generic; using System.Linq; +using System; namespace React { @@ -300,5 +301,11 @@ public IReactSiteConfiguration DisableServerSideRendering() UseServerSideRendering = false; return this; } + + /// + /// Handle an exception caught during server-render of a component. + /// If unset, unhandled exceptions will be thrown for all component renders. + /// + public Action HandleRenderException { get; set; } } } diff --git a/src/React.Sample.CoreMvc/Controllers/HomeController.cs b/src/React.Sample.CoreMvc/Controllers/HomeController.cs index 8dd83e224..5962890cd 100644 --- a/src/React.Sample.CoreMvc/Controllers/HomeController.cs +++ b/src/React.Sample.CoreMvc/Controllers/HomeController.cs @@ -37,6 +37,7 @@ public class IndexViewModel { public IEnumerable Comments { get; set; } public int CommentsPerPage { get; set; } + public bool ThrowRenderError { get; set; } } } @@ -78,7 +79,8 @@ public IActionResult Index() return View(new IndexViewModel { Comments = _comments.Take(COMMENTS_PER_PAGE), - CommentsPerPage = COMMENTS_PER_PAGE + CommentsPerPage = COMMENTS_PER_PAGE, + ThrowRenderError = Request.Query.ContainsKey("throwRenderError"), }); } diff --git a/src/React.Sample.CoreMvc/Views/Home/Index.cshtml b/src/React.Sample.CoreMvc/Views/Home/Index.cshtml index 3f65ab4e7..6f939ad17 100644 --- a/src/React.Sample.CoreMvc/Views/Home/Index.cshtml +++ b/src/React.Sample.CoreMvc/Views/Home/Index.cshtml @@ -14,7 +14,7 @@

- @Html.React("CommentsBox", new { initialComments = Model.Comments }) + @Html.React("CommentsBox", new { initialComments = Model.Comments, ThrowRenderError = Model.ThrowRenderError }) diff --git a/src/React.Sample.CoreMvc/wwwroot/js/Sample.jsx b/src/React.Sample.CoreMvc/wwwroot/js/Sample.jsx index 80aaedbe3..6fbea2727 100644 --- a/src/React.Sample.CoreMvc/wwwroot/js/Sample.jsx +++ b/src/React.Sample.CoreMvc/wwwroot/js/Sample.jsx @@ -9,7 +9,8 @@ class CommentsBox extends React.Component { static propTypes = { - initialComments: PropTypes.array.isRequired + initialComments: PropTypes.array.isRequired, + throwRenderError: PropTypes.bool, }; state = { @@ -53,6 +54,9 @@ class CommentsBox extends React.Component { {commentNodes} {this.renderMoreLink()} + + + ); } @@ -108,3 +112,46 @@ class Avatar extends React.Component { return 'https://avatars.githubusercontent.com/' + author.githubUsername + '?s=50'; } } + +class ErrorBoundary extends React.Component { + static propTypes = { + children: PropTypes.node.isRequired, + }; + + state = {}; + + componentDidCatch() { + this.setState({ hasCaughtException: true }); + } + + render() { + return this.state.hasCaughtException ? ( +
An error occurred. Please reload.
+ ) : this.props.children; + } +} + +class ExceptionDemo extends React.Component { + static propTypes = { + throwRenderError: PropTypes.bool, + } + + state = { + throwRenderError: this.props.throwRenderError, + }; + + onClick = () => { + window.history.replaceState(null, null, window.location + '?throwRenderError'); + this.setState({ throwRenderError: true }); + } + + render() { + return ( +
+ +
+ ); + } +} From 49706859da187d8667ec3cac2128fefdcb5099e2 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Wed, 15 Nov 2017 21:51:48 -0800 Subject: [PATCH 2/5] Add tests and update sample --- src/React.Core/IReactSiteConfiguration.cs | 18 +++++-- src/React.Core/ReactComponent.cs | 6 +-- src/React.Core/ReactSiteConfiguration.cs | 15 +++++- src/React.Sample.CoreMvc/Startup.cs | 19 +++++--- tests/React.Tests/Core/ReactComponentTest.cs | 51 +++++++++++++++++--- 5 files changed, 86 insertions(+), 23 deletions(-) diff --git a/src/React.Core/IReactSiteConfiguration.cs b/src/React.Core/IReactSiteConfiguration.cs index 6785d1705..cb6e41dfb 100644 --- a/src/React.Core/IReactSiteConfiguration.cs +++ b/src/React.Core/IReactSiteConfiguration.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -178,12 +178,20 @@ public interface IReactSiteConfiguration /// /// Disables server-side rendering. This is useful when debugging your scripts. /// - IReactSiteConfiguration DisableServerSideRendering(); - + IReactSiteConfiguration DisableServerSideRendering(); + /// - /// Handle an exception caught during server-render of a component. + /// An exception handler which will be called if a render exception is thrown. /// If unset, unhandled exceptions will be thrown for all component renders. /// - Action HandleRenderException { get; set; } + Action ExceptionHandler { get; set; } + + /// + /// Sets an exception handler which will be called if a render exception is thrown. + /// If unset, unhandled exceptions will be thrown for all component renders. + /// + /// + /// + IReactSiteConfiguration SetExceptionHandler(Action handler); } } diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs index 7e9e86bb1..cf941ca3b 100644 --- a/src/React.Core/ReactComponent.cs +++ b/src/React.Core/ReactComponent.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -132,7 +132,7 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe } catch (JsRuntimeException ex) { - if (_configuration.HandleRenderException == null) { + if (_configuration.ExceptionHandler == null) { throw new ReactServerRenderingException(string.Format( "Error while rendering \"{0}\" to \"{2}\": {1}", ComponentName, @@ -140,7 +140,7 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe ContainerId )); } - _configuration.HandleRenderException(ex); + _configuration.ExceptionHandler(ex); } } diff --git a/src/React.Core/ReactSiteConfiguration.cs b/src/React.Core/ReactSiteConfiguration.cs index ef10a855a..ee1850610 100644 --- a/src/React.Core/ReactSiteConfiguration.cs +++ b/src/React.Core/ReactSiteConfiguration.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -306,6 +306,17 @@ public IReactSiteConfiguration DisableServerSideRendering() /// Handle an exception caught during server-render of a component. /// If unset, unhandled exceptions will be thrown for all component renders. ///
- public Action HandleRenderException { get; set; } + public Action ExceptionHandler { get; set; } + + /// + /// + /// + /// + /// + public IReactSiteConfiguration SetExceptionHandler(Action handler) + { + ExceptionHandler = handler; + return this; + } } } diff --git a/src/React.Sample.CoreMvc/Startup.cs b/src/React.Sample.CoreMvc/Startup.cs index 356107449..fb70be38a 100644 --- a/src/React.Sample.CoreMvc/Startup.cs +++ b/src/React.Sample.CoreMvc/Startup.cs @@ -20,15 +20,16 @@ namespace React.Sample.CoreMvc { public class Startup { - public Startup(IHostingEnvironment env) - { - // Setup configuration sources. + public Startup(IHostingEnvironment env, ILogger logger) + { + // Setup configuration sources. var builder = new ConfigurationBuilder().AddEnvironmentVariables(); - + Logger = logger; Configuration = builder.Build(); } - public IConfiguration Configuration { get; set; } + public IConfiguration Configuration { get; set; } + public ILogger Logger { get; set; } // This method gets called by the runtime. public IServiceProvider ConfigureServices(IServiceCollection services) @@ -69,8 +70,12 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF { config .SetReuseJavaScriptEngines(true) - .AddScript("~/js/Sample.jsx") - .SetUseDebugReact(true); + .AddScript("~/js/Sample.jsx") + .SetExceptionHandler(ex => + { + Logger.LogError("React component exception thrown!" + ex.ToString()); + }) + .SetUseDebugReact(true); }); // Add static files to the request pipeline. diff --git a/tests/React.Tests/Core/ReactComponentTest.cs b/tests/React.Tests/Core/ReactComponentTest.cs index 8585345cf..e61a9c012 100644 --- a/tests/React.Tests/Core/ReactComponentTest.cs +++ b/tests/React.Tests/Core/ReactComponentTest.cs @@ -1,16 +1,18 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ - + */ + +using System; +using JavaScriptEngineSwitcher.Core; using Moq; -using Xunit; using React.Exceptions; - +using Xunit; + namespace React.Tests.Core { public class ReactComponentTest @@ -190,7 +192,44 @@ public void GeneratesContainerIdIfNotProvided() var component = new ReactComponent(environment.Object, config.Object, "Foo", null); Assert.StartsWith("react_", component.ContainerId); - } + } + + [Fact] + public void ExceptionThrownIsHandled() + { + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) + .Throws(new JsRuntimeException("'undefined' is not an object")); + + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" } + }; + + // Default behavior + bool exceptionCaught = false; + try + { + component.RenderHtml(); + } + catch (ReactServerRenderingException) + { + exceptionCaught = true; + } + + Assert.True(exceptionCaught); + + // Custom exception handler set + Exception caughtException = null; + config.Setup(x => x.ExceptionHandler).Returns(ex => caughtException = ex); + + var result = component.RenderHtml(); + Assert.Equal(@"
", result); + Assert.NotNull(caughtException); + } } } From ea071c8e152c9c694ac65ec0e4aa7b6f2b9bd6ca Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Wed, 15 Nov 2017 21:58:23 -0800 Subject: [PATCH 3/5] Fix newline/whitespace issues --- src/React.Core/IReactSiteConfiguration.cs | 20 +++--- src/React.Core/ReactSiteConfiguration.cs | 18 ++--- src/React.Sample.CoreMvc/Startup.cs | 16 ++--- tests/React.Tests/Core/ReactComponentTest.cs | 70 ++++++++++---------- 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/React.Core/IReactSiteConfiguration.cs b/src/React.Core/IReactSiteConfiguration.cs index cb6e41dfb..50b742eae 100644 --- a/src/React.Core/IReactSiteConfiguration.cs +++ b/src/React.Core/IReactSiteConfiguration.cs @@ -178,20 +178,20 @@ public interface IReactSiteConfiguration /// /// Disables server-side rendering. This is useful when debugging your scripts. /// - IReactSiteConfiguration DisableServerSideRendering(); - + IReactSiteConfiguration DisableServerSideRendering(); + /// /// An exception handler which will be called if a render exception is thrown. /// If unset, unhandled exceptions will be thrown for all component renders. /// - Action ExceptionHandler { get; set; } - - /// - /// Sets an exception handler which will be called if a render exception is thrown. - /// If unset, unhandled exceptions will be thrown for all component renders. - /// - /// - /// + Action ExceptionHandler { get; set; } + + /// + /// Sets an exception handler which will be called if a render exception is thrown. + /// If unset, unhandled exceptions will be thrown for all component renders. + /// + /// + /// IReactSiteConfiguration SetExceptionHandler(Action handler); } } diff --git a/src/React.Core/ReactSiteConfiguration.cs b/src/React.Core/ReactSiteConfiguration.cs index ee1850610..c2e78dc49 100644 --- a/src/React.Core/ReactSiteConfiguration.cs +++ b/src/React.Core/ReactSiteConfiguration.cs @@ -7,10 +7,10 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.Linq; -using System; +using Newtonsoft.Json; namespace React { @@ -306,13 +306,13 @@ public IReactSiteConfiguration DisableServerSideRendering() /// Handle an exception caught during server-render of a component. /// If unset, unhandled exceptions will be thrown for all component renders. /// - public Action ExceptionHandler { get; set; } - - /// - /// - /// - /// - /// + public Action ExceptionHandler { get; set; } + + /// + /// + /// + /// + /// public IReactSiteConfiguration SetExceptionHandler(Action handler) { ExceptionHandler = handler; diff --git a/src/React.Sample.CoreMvc/Startup.cs b/src/React.Sample.CoreMvc/Startup.cs index fb70be38a..9dcf3bde5 100644 --- a/src/React.Sample.CoreMvc/Startup.cs +++ b/src/React.Sample.CoreMvc/Startup.cs @@ -21,14 +21,14 @@ namespace React.Sample.CoreMvc public class Startup { public Startup(IHostingEnvironment env, ILogger logger) - { - // Setup configuration sources. + { + // Setup configuration sources. var builder = new ConfigurationBuilder().AddEnvironmentVariables(); Logger = logger; Configuration = builder.Build(); } - public IConfiguration Configuration { get; set; } + public IConfiguration Configuration { get; set; } public ILogger Logger { get; set; } // This method gets called by the runtime. @@ -70,12 +70,12 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF { config .SetReuseJavaScriptEngines(true) - .AddScript("~/js/Sample.jsx") - .SetExceptionHandler(ex => - { - Logger.LogError("React component exception thrown!" + ex.ToString()); + .AddScript("~/js/Sample.jsx") + .SetExceptionHandler(ex => + { + Logger.LogError("React component exception thrown!" + ex.ToString()); }) - .SetUseDebugReact(true); + .SetUseDebugReact(true); }); // Add static files to the request pipeline. diff --git a/tests/React.Tests/Core/ReactComponentTest.cs b/tests/React.Tests/Core/ReactComponentTest.cs index e61a9c012..be260d870 100644 --- a/tests/React.Tests/Core/ReactComponentTest.cs +++ b/tests/React.Tests/Core/ReactComponentTest.cs @@ -5,14 +5,14 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ - -using System; -using JavaScriptEngineSwitcher.Core; + */ + +using System; +using JavaScriptEngineSwitcher.Core; using Moq; using React.Exceptions; using Xunit; - + namespace React.Tests.Core { public class ReactComponentTest @@ -162,7 +162,7 @@ public void RenderJavaScriptShouldCallRenderComponent() ); } - [Theory] + [Theory] [InlineData("Foo", true)] [InlineData("Foo.Bar", true)] [InlineData("Foo.Bar.Baz", true)] @@ -192,43 +192,43 @@ public void GeneratesContainerIdIfNotProvided() var component = new ReactComponent(environment.Object, config.Object, "Foo", null); Assert.StartsWith("react_", component.ContainerId); - } - - [Fact] - public void ExceptionThrownIsHandled() - { + } + + [Fact] + public void ExceptionThrownIsHandled() + { var environment = new Mock(); environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) - .Throws(new JsRuntimeException("'undefined' is not an object")); - - var config = new Mock(); + .Throws(new JsRuntimeException("'undefined' is not an object")); + + var config = new Mock(); config.Setup(x => x.UseServerSideRendering).Returns(true); var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") { Props = new { hello = "World" } - }; - - // Default behavior - bool exceptionCaught = false; - try - { - component.RenderHtml(); - } - catch (ReactServerRenderingException) - { - exceptionCaught = true; - } - - Assert.True(exceptionCaught); - - // Custom exception handler set - Exception caughtException = null; - config.Setup(x => x.ExceptionHandler).Returns(ex => caughtException = ex); - - var result = component.RenderHtml(); - Assert.Equal(@"
", result); + }; + + // Default behavior + bool exceptionCaught = false; + try + { + component.RenderHtml(); + } + catch (ReactServerRenderingException) + { + exceptionCaught = true; + } + + Assert.True(exceptionCaught); + + // Custom exception handler set + Exception caughtException = null; + config.Setup(x => x.ExceptionHandler).Returns(ex => caughtException = ex); + + var result = component.RenderHtml(); + Assert.Equal(@"
", result); Assert.NotNull(caughtException); } } From 46b45b3373f761cdf008828ae86978f61a8f4151 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Tue, 19 Dec 2017 07:55:40 -0800 Subject: [PATCH 4/5] Support component-level exception handlers --- src/React.AspNet/HtmlHelperExtensions.cs | 11 +++++---- src/React.Core/IReactComponent.cs | 13 ++++++---- src/React.Core/IReactSiteConfiguration.cs | 4 ++-- src/React.Core/ReactComponent.cs | 24 +++++++++---------- src/React.Core/ReactSiteConfiguration.cs | 12 ++++++++-- src/React.Sample.CoreMvc/Startup.cs | 6 ++--- tests/React.Tests/Core/ReactComponentTest.cs | 13 +++++++--- .../Mvc/HtmlHelperExtensionsTests.cs | 20 ++++++++-------- 8 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index cb6b168c9..66e6a602c 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -7,6 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +using System; using React.Exceptions; using React.TinyIoC; @@ -64,7 +65,8 @@ public static IHtmlString React( string containerId = null, bool clientOnly = false, bool serverOnly = false, - string containerClass = null + string containerClass = null, + Action exceptionHandler = null ) { try @@ -78,7 +80,7 @@ public static IHtmlString React( { reactComponent.ContainerClass = containerClass; } - var result = reactComponent.RenderHtml(clientOnly, serverOnly); + var result = reactComponent.RenderHtml(clientOnly, serverOnly, exceptionHandler); return new HtmlString(result); } finally @@ -108,7 +110,8 @@ public static IHtmlString ReactWithInit( string htmlTag = null, string containerId = null, bool clientOnly = false, - string containerClass = null + string containerClass = null, + Action exceptionHandler = null ) { try @@ -122,7 +125,7 @@ public static IHtmlString ReactWithInit( { reactComponent.ContainerClass = containerClass; } - var html = reactComponent.RenderHtml(clientOnly); + var html = reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler); #if LEGACYASPNET var script = new TagBuilder("script") diff --git a/src/React.Core/IReactComponent.cs b/src/React.Core/IReactComponent.cs index 04016d09a..48078a760 100644 --- a/src/React.Core/IReactComponent.cs +++ b/src/React.Core/IReactComponent.cs @@ -1,14 +1,16 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ + */ + +using System; namespace React -{ +{ /// /// Represents a React JavaScript component. /// @@ -44,9 +46,10 @@ public interface IReactComponent /// return the rendered HTML. /// /// 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. + /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering. + /// /// HTML - string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false); + string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action exceptionHandler = null); /// /// Renders the JavaScript required to initialise this component client-side. This will diff --git a/src/React.Core/IReactSiteConfiguration.cs b/src/React.Core/IReactSiteConfiguration.cs index 50b742eae..8c3eed6da 100644 --- a/src/React.Core/IReactSiteConfiguration.cs +++ b/src/React.Core/IReactSiteConfiguration.cs @@ -184,7 +184,7 @@ public interface IReactSiteConfiguration /// An exception handler which will be called if a render exception is thrown. /// If unset, unhandled exceptions will be thrown for all component renders. /// - Action ExceptionHandler { get; set; } + Action ExceptionHandler { get; set; } /// /// Sets an exception handler which will be called if a render exception is thrown. @@ -192,6 +192,6 @@ public interface IReactSiteConfiguration /// /// /// - IReactSiteConfiguration SetExceptionHandler(Action handler); + IReactSiteConfiguration SetExceptionHandler(Action handler); } } diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs index cf941ca3b..2ebe4a7b8 100644 --- a/src/React.Core/ReactComponent.cs +++ b/src/React.Core/ReactComponent.cs @@ -5,8 +5,8 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ - + */ + using System; using System.Linq; using System.Text.RegularExpressions; @@ -15,7 +15,7 @@ using React.Exceptions; namespace React -{ +{ /// /// Represents a React JavaScript component. /// @@ -106,9 +106,10 @@ public ReactComponent(IReactEnvironment environment, IReactSiteConfiguration con /// return the rendered HTML. /// /// 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. + /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering. + /// /// HTML - public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false) + public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action exceptionHandler = null) { if (!_configuration.UseServerSideRendering) { @@ -132,15 +133,12 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe } catch (JsRuntimeException ex) { - if (_configuration.ExceptionHandler == null) { - throw new ReactServerRenderingException(string.Format( - "Error while rendering \"{0}\" to \"{2}\": {1}", - ComponentName, - ex.Message, - ContainerId - )); + if (exceptionHandler == null) + { + exceptionHandler = _configuration.ExceptionHandler; } - _configuration.ExceptionHandler(ex); + + exceptionHandler(ex, ComponentName, ContainerId); } } diff --git a/src/React.Core/ReactSiteConfiguration.cs b/src/React.Core/ReactSiteConfiguration.cs index c2e78dc49..879a92db9 100644 --- a/src/React.Core/ReactSiteConfiguration.cs +++ b/src/React.Core/ReactSiteConfiguration.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; +using React.Exceptions; namespace React { @@ -45,6 +46,13 @@ public ReactSiteConfiguration() }; UseDebugReact = false; UseServerSideRendering = true; + ExceptionHandler = (Exception ex, string ComponentName, string ContainerId) => + throw new ReactServerRenderingException(string.Format( + "Error while rendering \"{0}\" to \"{2}\": {1}", + ComponentName, + ex.Message, + ContainerId + )); } /// @@ -306,14 +314,14 @@ public IReactSiteConfiguration DisableServerSideRendering() /// Handle an exception caught during server-render of a component. /// If unset, unhandled exceptions will be thrown for all component renders. /// - public Action ExceptionHandler { get; set; } + public Action ExceptionHandler { get; set; } /// /// /// /// /// - public IReactSiteConfiguration SetExceptionHandler(Action handler) + public IReactSiteConfiguration SetExceptionHandler(Action handler) { ExceptionHandler = handler; return this; diff --git a/src/React.Sample.CoreMvc/Startup.cs b/src/React.Sample.CoreMvc/Startup.cs index 9dcf3bde5..d4b0da082 100644 --- a/src/React.Sample.CoreMvc/Startup.cs +++ b/src/React.Sample.CoreMvc/Startup.cs @@ -5,8 +5,8 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ - + */ + using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -71,7 +71,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF config .SetReuseJavaScriptEngines(true) .AddScript("~/js/Sample.jsx") - .SetExceptionHandler(ex => + .SetExceptionHandler((ex, name, id) => { Logger.LogError("React component exception thrown!" + ex.ToString()); }) diff --git a/tests/React.Tests/Core/ReactComponentTest.cs b/tests/React.Tests/Core/ReactComponentTest.cs index be260d870..234825b05 100644 --- a/tests/React.Tests/Core/ReactComponentTest.cs +++ b/tests/React.Tests/Core/ReactComponentTest.cs @@ -5,8 +5,8 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ - + */ + using System; using JavaScriptEngineSwitcher.Core; using Moq; @@ -204,6 +204,7 @@ public void ExceptionThrownIsHandled() var config = new Mock(); config.Setup(x => x.UseServerSideRendering).Returns(true); + config.Setup(x => x.ExceptionHandler).Returns(() => throw new ReactServerRenderingException("test")); var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") { @@ -223,9 +224,15 @@ public void ExceptionThrownIsHandled() Assert.True(exceptionCaught); + // Custom handler passed into render call + bool customHandlerInvoked = false; + Action customHandler = (ex, name, id) => customHandlerInvoked = true; + component.RenderHtml(exceptionHandler: customHandler); + Assert.True(customHandlerInvoked); + // Custom exception handler set Exception caughtException = null; - config.Setup(x => x.ExceptionHandler).Returns(ex => caughtException = ex); + config.Setup(x => x.ExceptionHandler).Returns((ex, name, id) => caughtException = ex); var result = component.RenderHtml(); Assert.Equal(@"
", result); diff --git a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs index f087fdd77..14d1253a4 100644 --- a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs +++ b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs @@ -5,11 +5,11 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ - + */ + using Moq; -using Xunit; using React.Web.Mvc; +using Xunit; namespace React.Tests.Mvc { @@ -31,7 +31,7 @@ private Mock ConfigureMockEnvironment() public void ReactWithInitShouldReturnHtmlAndScript() { var component = new Mock(); - component.Setup(x => x.RenderHtml(false, false)).Returns("HTML"); + component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML"); component.Setup(x => x.RenderJavaScript()).Returns("JS"); var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( @@ -57,7 +57,7 @@ public void ReactWithInitShouldReturnHtmlAndScript() public void EngineIsReturnedToPoolAfterRender() { var component = new Mock(); - component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML"); var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( "ComponentName", @@ -75,7 +75,7 @@ public void EngineIsReturnedToPoolAfterRender() clientOnly: true, serverOnly: true ); - component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); + component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true), null), Times.Once); environment.Verify(x => x.ReturnEngineToPool(), Times.Once); } @@ -83,7 +83,7 @@ public void EngineIsReturnedToPoolAfterRender() public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue() { var component = new Mock(); - component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML"); var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( "ComponentName", @@ -100,13 +100,13 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue() clientOnly: true, serverOnly: true ); - component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); + component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true), null), Times.Once); } [Fact] public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() { var component = new Mock(); - component.Setup(x => x.RenderHtml(true, true)).Returns("HTML"); + component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML"); var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( "ComponentName", @@ -123,7 +123,7 @@ public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() { clientOnly: true, serverOnly: true ); - component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true)), Times.Once); + component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true), null), Times.Once); } } } From 13c99266fca73bf5c1dc9d594aa6a76b4c125bd1 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Tue, 19 Dec 2017 07:58:50 -0800 Subject: [PATCH 5/5] Document exceptionHandler arguments --- src/React.AspNet/HtmlHelperExtensions.cs | 2 ++ src/React.Core/IReactComponent.cs | 10 +++++----- src/React.Core/ReactComponent.cs | 10 +++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index 66e6a602c..b136ca42f 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -56,6 +56,7 @@ private static IReactEnvironment Environment /// Skip rendering server-side and only output client-side initialisation code. Defaults to false /// Skip rendering React specific data-attributes during server side rendering. Defaults to false /// HTML class(es) to set on the container tag + /// A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId) /// The component's HTML public static IHtmlString React( this IHtmlHelper htmlHelper, @@ -102,6 +103,7 @@ public static IHtmlString React( /// ID to use for the container HTML tag. Defaults to an auto-generated ID /// Skip rendering server-side and only output client-side initialisation code. Defaults to false /// HTML class(es) to set on the container tag + /// A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId) /// The component's HTML public static IHtmlString ReactWithInit( this IHtmlHelper htmlHelper, diff --git a/src/React.Core/IReactComponent.cs b/src/React.Core/IReactComponent.cs index 48078a760..3cdf7f794 100644 --- a/src/React.Core/IReactComponent.cs +++ b/src/React.Core/IReactComponent.cs @@ -5,12 +5,12 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ - + */ + using System; namespace React -{ +{ /// /// Represents a React JavaScript component. /// @@ -46,8 +46,8 @@ public interface IReactComponent /// return the rendered HTML. /// /// 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. - /// + /// 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 string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action exceptionHandler = null); diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs index 2ebe4a7b8..e736604f0 100644 --- a/src/React.Core/ReactComponent.cs +++ b/src/React.Core/ReactComponent.cs @@ -5,8 +5,8 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. - */ - + */ + using System; using System.Linq; using System.Text.RegularExpressions; @@ -15,7 +15,7 @@ using React.Exceptions; namespace React -{ +{ /// /// Represents a React JavaScript component. /// @@ -106,8 +106,8 @@ public ReactComponent(IReactEnvironment environment, IReactSiteConfiguration con /// return the rendered HTML. /// /// 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. - /// + /// 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 string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action exceptionHandler = null) {