Skip to content

Commit b656cc0

Browse files
Add support for exception handling during component render
1 parent 2e6b9a7 commit b656cc0

File tree

6 files changed

+89
-24
lines changed

6 files changed

+89
-24
lines changed

src/React.Core/IReactSiteConfiguration.cs

+6
Original file line numberDiff line numberDiff line change
@@ -179,5 +179,11 @@ public interface IReactSiteConfiguration
179179
/// Disables server-side rendering. This is useful when debugging your scripts.
180180
/// </summary>
181181
IReactSiteConfiguration DisableServerSideRendering();
182+
183+
/// <summary>
184+
/// Handle an exception caught during server-render of a component.
185+
/// If unset, unhandled exceptions will be thrown for all component renders.
186+
/// </summary>
187+
Action<Exception> HandleRenderException { get; set; }
182188
}
183189
}

src/React.Core/ReactComponent.cs

+24-21
Original file line numberDiff line numberDiff line change
@@ -120,39 +120,42 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
120120
EnsureComponentExists();
121121
}
122122

123-
try
123+
var html = string.Empty;
124+
if (!renderContainerOnly)
124125
{
125-
var html = string.Empty;
126-
if (!renderContainerOnly)
126+
try
127127
{
128128
var reactRenderCommand = renderServerOnly
129129
? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser())
130130
: string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser());
131131
html = _environment.Execute<string>(reactRenderCommand);
132132
}
133-
134-
string attributes = string.Format("id=\"{0}\"", ContainerId);
135-
if (!string.IsNullOrEmpty(ContainerClass))
133+
catch (JsRuntimeException ex)
136134
{
137-
attributes += string.Format(" class=\"{0}\"", ContainerClass);
135+
if (_configuration.HandleRenderException == null) {
136+
throw new ReactServerRenderingException(string.Format(
137+
"Error while rendering \"{0}\" to \"{2}\": {1}",
138+
ComponentName,
139+
ex.Message,
140+
ContainerId
141+
));
142+
}
143+
_configuration.HandleRenderException(ex);
138144
}
139-
140-
return string.Format(
141-
"<{2} {0}>{1}</{2}>",
142-
attributes,
143-
html,
144-
ContainerTag
145-
);
146145
}
147-
catch (JsRuntimeException ex)
146+
147+
string attributes = string.Format("id=\"{0}\"", ContainerId);
148+
if (!string.IsNullOrEmpty(ContainerClass))
148149
{
149-
throw new ReactServerRenderingException(string.Format(
150-
"Error while rendering \"{0}\" to \"{2}\": {1}",
151-
ComponentName,
152-
ex.Message,
153-
ContainerId
154-
));
150+
attributes += string.Format(" class=\"{0}\"", ContainerClass);
155151
}
152+
153+
return string.Format(
154+
"<{2} {0}>{1}</{2}>",
155+
attributes,
156+
html,
157+
ContainerTag
158+
);
156159
}
157160

158161
/// <summary>

src/React.Core/ReactSiteConfiguration.cs

+7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Newtonsoft.Json;
1111
using System.Collections.Generic;
1212
using System.Linq;
13+
using System;
1314

1415
namespace React
1516
{
@@ -300,5 +301,11 @@ public IReactSiteConfiguration DisableServerSideRendering()
300301
UseServerSideRendering = false;
301302
return this;
302303
}
304+
305+
/// <summary>
306+
/// Handle an exception caught during server-render of a component.
307+
/// If unset, unhandled exceptions will be thrown for all component renders.
308+
/// </summary>
309+
public Action<Exception> HandleRenderException { get; set; }
303310
}
304311
}

src/React.Sample.CoreMvc/Controllers/HomeController.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public class IndexViewModel
3737
{
3838
public IEnumerable<CommentModel> Comments { get; set; }
3939
public int CommentsPerPage { get; set; }
40+
public bool ThrowRenderError { get; set; }
4041
}
4142
}
4243

@@ -78,7 +79,8 @@ public IActionResult Index()
7879
return View(new IndexViewModel
7980
{
8081
Comments = _comments.Take(COMMENTS_PER_PAGE),
81-
CommentsPerPage = COMMENTS_PER_PAGE
82+
CommentsPerPage = COMMENTS_PER_PAGE,
83+
ThrowRenderError = Request.Query.ContainsKey("throwRenderError"),
8284
});
8385
}
8486

src/React.Sample.CoreMvc/Views/Home/Index.cshtml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
</p>
1515

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

1919
<!-- Load all required scripts (React + the site's scripts) -->
2020
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.development.js"></script>

src/React.Sample.CoreMvc/wwwroot/js/Sample.jsx

+48-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99

1010
class CommentsBox extends React.Component {
1111
static propTypes = {
12-
initialComments: PropTypes.array.isRequired
12+
initialComments: PropTypes.array.isRequired,
13+
throwRenderError: PropTypes.bool,
1314
};
1415

1516
state = {
@@ -53,6 +54,9 @@ class CommentsBox extends React.Component {
5354
{commentNodes}
5455
</ol>
5556
{this.renderMoreLink()}
57+
<ErrorBoundary>
58+
<ExceptionDemo throwRenderError={this.props.throwRenderError} />
59+
</ErrorBoundary>
5660
</div>
5761
);
5862
}
@@ -108,3 +112,46 @@ class Avatar extends React.Component {
108112
return 'https://avatars.githubusercontent.com/' + author.githubUsername + '?s=50';
109113
}
110114
}
115+
116+
class ErrorBoundary extends React.Component {
117+
static propTypes = {
118+
children: PropTypes.node.isRequired,
119+
};
120+
121+
state = {};
122+
123+
componentDidCatch() {
124+
this.setState({ hasCaughtException: true });
125+
}
126+
127+
render() {
128+
return this.state.hasCaughtException ? (
129+
<div>An error occurred. Please reload.</div>
130+
) : this.props.children;
131+
}
132+
}
133+
134+
class ExceptionDemo extends React.Component {
135+
static propTypes = {
136+
throwRenderError: PropTypes.bool,
137+
}
138+
139+
state = {
140+
throwRenderError: this.props.throwRenderError,
141+
};
142+
143+
onClick = () => {
144+
window.history.replaceState(null, null, window.location + '?throwRenderError');
145+
this.setState({ throwRenderError: true });
146+
}
147+
148+
render() {
149+
return (
150+
<div>
151+
<button onClick={this.onClick}>
152+
{this.state.throwRenderError ? this.state.testObject.one.two : ''}Throw exception
153+
</button>
154+
</div>
155+
);
156+
}
157+
}

0 commit comments

Comments
 (0)