Skip to content

Commit

Permalink
Add support for exception handling during component render
Browse files Browse the repository at this point in the history
  • Loading branch information
dustinsoftware committed Nov 12, 2017
1 parent 2e6b9a7 commit b656cc0
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 24 deletions.
6 changes: 6 additions & 0 deletions src/React.Core/IReactSiteConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,5 +179,11 @@ public interface IReactSiteConfiguration
/// Disables server-side rendering. This is useful when debugging your scripts.
/// </summary>
IReactSiteConfiguration DisableServerSideRendering();

/// <summary>
/// Handle an exception caught during server-render of a component.
/// If unset, unhandled exceptions will be thrown for all component renders.
/// </summary>
Action<Exception> HandleRenderException { get; set; }
}
}
45 changes: 24 additions & 21 deletions src/React.Core/ReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(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}</{2}>",
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}</{2}>",
attributes,
html,
ContainerTag
);
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions src/React.Core/ReactSiteConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using System;

namespace React
{
Expand Down Expand Up @@ -300,5 +301,11 @@ public IReactSiteConfiguration DisableServerSideRendering()
UseServerSideRendering = false;
return this;
}

/// <summary>
/// Handle an exception caught during server-render of a component.
/// If unset, unhandled exceptions will be thrown for all component renders.
/// </summary>
public Action<Exception> HandleRenderException { get; set; }
}
}
4 changes: 3 additions & 1 deletion src/React.Sample.CoreMvc/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class IndexViewModel
{
public IEnumerable<CommentModel> Comments { get; set; }
public int CommentsPerPage { get; set; }
public bool ThrowRenderError { get; set; }
}
}

Expand Down Expand Up @@ -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"),
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/React.Sample.CoreMvc/Views/Home/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</p>

<!-- Render the component server-side, passing initial props -->
@Html.React("CommentsBox", new { initialComments = Model.Comments })
@Html.React("CommentsBox", new { initialComments = Model.Comments, ThrowRenderError = Model.ThrowRenderError })

<!-- Load all required scripts (React + the site's scripts) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.development.js"></script>
Expand Down
49 changes: 48 additions & 1 deletion src/React.Sample.CoreMvc/wwwroot/js/Sample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@

class CommentsBox extends React.Component {
static propTypes = {
initialComments: PropTypes.array.isRequired
initialComments: PropTypes.array.isRequired,
throwRenderError: PropTypes.bool,
};

state = {
Expand Down Expand Up @@ -53,6 +54,9 @@ class CommentsBox extends React.Component {
{commentNodes}
</ol>
{this.renderMoreLink()}
<ErrorBoundary>
<ExceptionDemo throwRenderError={this.props.throwRenderError} />
</ErrorBoundary>
</div>
);
}
Expand Down Expand Up @@ -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 ? (
<div>An error occurred. Please reload.</div>
) : 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 (
<div>
<button onClick={this.onClick}>
{this.state.throwRenderError ? this.state.testObject.one.two : ''}Throw exception
</button>
</div>
);
}
}

0 comments on commit b656cc0

Please sign in to comment.