Skip to content

Commit d3d8edd

Browse files
Lazily initialize component JS when using Html.ReactWithInit
This allows Output Caching to be used, in #543 Still needs tests and documentation updates
1 parent 60a61e9 commit d3d8edd

10 files changed

+118
-64
lines changed

Diff for: src/React.AspNet/HtmlHelperExtensions.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,10 @@ public static IHtmlString React<T>(
104104
/// <param name="htmlTag">HTML tag to wrap the component in. Defaults to &lt;div&gt;</param>
105105
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
106106
/// <param name="clientOnly">Skip rendering server-side and only output client-side initialisation code. Defaults to <c>false</c></param>
107+
/// <param name="serverOnly">Skip rendering React specific data-attributes, container and client-side initialisation during server side rendering. Defaults to <c>false</c></param>
107108
/// <param name="containerClass">HTML class(es) to set on the container tag</param>
108109
/// <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>
110+
/// <param name="renderFunctions"></param>
109111
/// <returns>The component's HTML</returns>
110112
public static IHtmlString ReactWithInit<T>(
111113
this IHtmlHelper htmlHelper,
@@ -114,8 +116,10 @@ public static IHtmlString ReactWithInit<T>(
114116
string htmlTag = null,
115117
string containerId = null,
116118
bool clientOnly = false,
119+
bool serverOnly = false,
117120
string containerClass = null,
118-
Action<Exception, string, string> exceptionHandler = null
121+
Action<Exception, string, string> exceptionHandler = null,
122+
IRenderFunctions renderFunctions = null
119123
)
120124
{
121125
try
@@ -133,11 +137,11 @@ public static IHtmlString ReactWithInit<T>(
133137

134138
return RenderToString(writer =>
135139
{
136-
reactComponent.RenderHtml(writer, clientOnly, exceptionHandler: exceptionHandler);
140+
reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler: exceptionHandler, renderFunctions);
137141
writer.WriteLine();
138-
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter));
142+
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter, waitForDOMContentLoad: true));
139143
});
140-
144+
141145
}
142146
finally
143147
{

Diff for: src/React.Core/IReactComponent.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,6 @@ public interface IReactComponent
5656
/// <returns>HTML</returns>
5757
string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null, IRenderFunctions renderFunctions = null);
5858

59-
/// <summary>
60-
/// Renders the JavaScript required to initialise this component client-side. This will
61-
/// initialise the React component, which includes attach event handlers to the
62-
/// server-rendered HTML.
63-
/// </summary>
64-
/// <returns>JavaScript</returns>
65-
string RenderJavaScript();
66-
6759
/// <summary>
6860
/// Renders the HTML for this component. This will execute the component server-side and
6961
/// return the rendered HTML.
@@ -82,6 +74,14 @@ public interface IReactComponent
8274
/// server-rendered HTML.
8375
/// </summary>
8476
/// <returns>JavaScript</returns>
85-
void RenderJavaScript(TextWriter writer);
77+
string RenderJavaScript();
78+
79+
/// <summary>
80+
/// Renders the JavaScript required to initialise this component client-side. This will
81+
/// initialise the React component, which includes attach event handlers to the
82+
/// server-rendered HTML.
83+
/// </summary>
84+
/// <returns>JavaScript</returns>
85+
void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad);
8686
}
8787
}

Diff for: src/React.Core/IReactEnvironment.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ public interface IReactEnvironment
100100
/// <returns>JavaScript for all components</returns>
101101
string GetInitJavaScript(bool clientOnly = false);
102102

103+
/// <summary>
104+
/// Renders the JavaScript required to initialise all components client-side. This will
105+
/// attach event handlers to the server-rendered HTML.
106+
/// </summary>
107+
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
108+
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
109+
/// <returns>JavaScript for all components</returns>
110+
void GetInitJavaScript(TextWriter writer, bool clientOnly = false);
111+
103112
/// <summary>
104113
/// Gets the JSX Transformer for this environment.
105114
/// </summary>
@@ -114,14 +123,5 @@ public interface IReactEnvironment
114123
/// Gets the site-wide configuration.
115124
/// </summary>
116125
IReactSiteConfiguration Configuration { get; }
117-
118-
/// <summary>
119-
/// Renders the JavaScript required to initialise all components client-side. This will
120-
/// attach event handlers to the server-rendered HTML.
121-
/// </summary>
122-
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
123-
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
124-
/// <returns>JavaScript for all components</returns>
125-
void GetInitJavaScript(TextWriter writer, bool clientOnly = false);
126126
}
127127
}

Diff for: src/React.Core/IReactSiteConfiguration.cs

+13-13
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public interface IReactSiteConfiguration
1919
/// <summary>
2020
/// Adds a script to the list of scripts that are executed. This should be called for all
2121
/// React components and their dependencies. If the script does not have any JSX in it
22-
/// (for example, it's built using Webpack or Gulp), use
22+
/// (for example, it's built using Webpack or Gulp), use
2323
/// <see cref="AddScriptWithoutTransform"/> instead.
2424
/// </summary>
2525
/// <param name="filename">
26-
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
26+
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
2727
/// <c>~/Scripts/Awesome.js</c>)
2828
/// </param>
2929
/// <returns>This configuration, for chaining</returns>
@@ -35,7 +35,7 @@ public interface IReactSiteConfiguration
3535
/// more efficient.
3636
/// </summary>
3737
/// <param name="filename">
38-
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
38+
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
3939
/// <c>~/Scripts/Awesome.js</c>)
4040
/// </param>
4141
/// <returns>The configuration, for chaining</returns>
@@ -48,15 +48,15 @@ public interface IReactSiteConfiguration
4848
IEnumerable<string> Scripts { get; }
4949

5050
/// <summary>
51-
/// Gets a list of all the scripts that have been added to this configuration and do not
51+
/// Gets a list of all the scripts that have been added to this configuration and do not
5252
/// require JSX transformation to be run.
5353
/// </summary>
54-
IEnumerable<string> ScriptsWithoutTransform { get; }
54+
IEnumerable<string> ScriptsWithoutTransform { get; }
5555

5656
/// <summary>
5757
/// Gets or sets whether JavaScript engines should be reused across requests.
5858
/// </summary>
59-
///
59+
///
6060
bool ReuseJavaScriptEngines { get; set; }
6161
/// <summary>
6262
/// Sets whether JavaScript engines should be reused across requests.
@@ -79,23 +79,23 @@ public interface IReactSiteConfiguration
7979
IReactSiteConfiguration SetJsonSerializerSettings(JsonSerializerSettings settings);
8080

8181
/// <summary>
82-
/// Gets or sets the number of engines to initially start when a pool is created.
82+
/// Gets or sets the number of engines to initially start when a pool is created.
8383
/// Defaults to <c>10</c>.
8484
/// </summary>
8585
int? StartEngines { get; set; }
8686
/// <summary>
87-
/// Sets the number of engines to initially start when a pool is created.
87+
/// Sets the number of engines to initially start when a pool is created.
8888
/// Defaults to <c>10</c>.
8989
/// </summary>
9090
IReactSiteConfiguration SetStartEngines(int? startEngines);
9191

9292
/// <summary>
93-
/// Gets or sets the maximum number of engines that will be created in the pool.
93+
/// Gets or sets the maximum number of engines that will be created in the pool.
9494
/// Defaults to <c>25</c>.
9595
/// </summary>
9696
int? MaxEngines { get; set; }
9797
/// <summary>
98-
/// Sets the maximum number of engines that will be created in the pool.
98+
/// Sets the maximum number of engines that will be created in the pool.
9999
/// Defaults to <c>25</c>.
100100
/// </summary>
101101
IReactSiteConfiguration SetMaxEngines(int? maxEngines);
@@ -129,7 +129,7 @@ public interface IReactSiteConfiguration
129129
/// </summary>
130130
bool LoadReact { get; set; }
131131
/// <summary>
132-
/// Sets whether the built-in version of React is loaded. If <c>false</c>, you must
132+
/// Sets whether the built-in version of React is loaded. If <c>false</c>, you must
133133
/// provide your own version of React.
134134
/// </summary>
135135
/// <returns>The configuration, for chaining</returns>
@@ -204,13 +204,13 @@ public interface IReactSiteConfiguration
204204
IReactSiteConfiguration SetExceptionHandler(Action<Exception, string, string> handler);
205205

206206
/// <summary>
207-
/// A provider that returns a nonce to be used on any script tags on the page.
207+
/// A provider that returns a nonce to be used on any script tags on the page.
208208
/// This value must match the nonce used in the Content Security Policy header on the response.
209209
/// </summary>
210210
Func<string> ScriptNonceProvider { get; set; }
211211

212212
/// <summary>
213-
/// Sets a provider that returns a nonce to be used on any script tags on the page.
213+
/// Sets a provider that returns a nonce to be used on any script tags on the page.
214214
/// This value must match the nonce used in the Content Security Policy header on the response.
215215
/// </summary>
216216
/// <param name="provider"></param>

Diff for: src/React.Core/ReactComponent.cs

+26-10
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,12 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
140140
/// <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>
141141
/// <param name="renderFunctions">Functions to call during component render</param>
142142
/// <returns>HTML</returns>
143-
public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null, IRenderFunctions renderFunctions = null)
143+
public virtual void RenderHtml(
144+
TextWriter writer,
145+
bool renderContainerOnly = false,
146+
bool renderServerOnly = false,
147+
Action<Exception, string, string> exceptionHandler = null,
148+
IRenderFunctions renderFunctions = null)
144149
{
145150
if (!_configuration.UseServerSideRendering)
146151
{
@@ -233,24 +238,35 @@ public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = fal
233238
/// <returns>JavaScript</returns>
234239
public virtual string RenderJavaScript()
235240
{
236-
return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter));
241+
return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter, waitForDOMContentLoad: false));
237242
}
238243

239-
/// <summary>
240-
/// Renders the JavaScript required to initialise this component client-side. This will
241-
/// initialise the React component, which includes attach event handlers to the
242-
/// server-rendered HTML.
243-
/// </summary>
244-
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
245-
/// <returns>JavaScript</returns>
246-
public virtual void RenderJavaScript(TextWriter writer)
244+
/// <summary>
245+
/// Renders the JavaScript required to initialise this component client-side. This will
246+
/// initialise the React component, which includes attach event handlers to the
247+
/// server-rendered HTML.
248+
/// </summary>
249+
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
250+
/// <param name="waitForDOMContentLoad">Delays the component init until the page load event fires. Useful if the component script tags are located after the call to Html.ReactWithInit. </param>
251+
/// <returns>JavaScript</returns>
252+
public virtual void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad)
247253
{
254+
if (waitForDOMContentLoad)
255+
{
256+
writer.Write("window.addEventListener('DOMContentLoaded', function() {");
257+
}
258+
248259
writer.Write(
249260
!_configuration.UseServerSideRendering || ClientOnly ? "ReactDOM.render(" : "ReactDOM.hydrate(");
250261
WriteComponentInitialiser(writer);
251262
writer.Write(", document.getElementById(\"");
252263
writer.Write(ContainerId);
253264
writer.Write("\"))");
265+
266+
if (waitForDOMContentLoad)
267+
{
268+
writer.Write("});");
269+
}
254270
}
255271

256272
/// <summary>

Diff for: src/React.Core/ReactEnvironment.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ public virtual void GetInitJavaScript(TextWriter writer, bool clientOnly = false
342342
{
343343
if (!component.ServerOnly)
344344
{
345-
component.RenderJavaScript(writer);
345+
component.RenderJavaScript(writer, waitForDOMContentLoad: true);
346346
writer.WriteLine(';');
347347
}
348348
}

Diff for: src/React.Core/ReactSiteConfiguration.cs

+13-13
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public ReactSiteConfiguration()
4444
};
4545
UseDebugReact = false;
4646
UseServerSideRendering = true;
47-
ExceptionHandler = (Exception ex, string ComponentName, string ContainerId) =>
47+
ExceptionHandler = (Exception ex, string ComponentName, string ContainerId) =>
4848
throw new ReactServerRenderingException(string.Format(
4949
"Error while rendering \"{0}\" to \"{2}\": {1}",
5050
ComponentName,
@@ -54,7 +54,7 @@ public ReactSiteConfiguration()
5454
}
5555

5656
/// <summary>
57-
/// All the scripts that have been added to this configuration and require JSX
57+
/// All the scripts that have been added to this configuration and require JSX
5858
/// transformation to be run.
5959
/// </summary>
6060
private readonly IList<string> _scriptFiles = new List<string>();
@@ -67,11 +67,11 @@ public ReactSiteConfiguration()
6767
/// <summary>
6868
/// Adds a script to the list of scripts that are executed. This should be called for all
6969
/// React components and their dependencies. If the script does not have any JSX in it
70-
/// (for example, it's built using Webpack or Gulp), use
70+
/// (for example, it's built using Webpack or Gulp), use
7171
/// <see cref="AddScriptWithoutTransform"/> instead.
7272
/// </summary>
7373
/// <param name="filename">
74-
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
74+
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
7575
/// <c>~/Scripts/Awesome.js</c>)
7676
/// </param>
7777
/// <returns>This configuration, for chaining</returns>
@@ -87,7 +87,7 @@ public IReactSiteConfiguration AddScript(string filename)
8787
/// more efficient.
8888
/// </summary>
8989
/// <param name="filename">
90-
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
90+
/// Name of the file to execute. Should be a server relative path starting with ~ (eg.
9191
/// <c>~/Scripts/Awesome.js</c>)
9292
/// </param>
9393
/// <returns>The configuration, for chaining</returns>
@@ -169,12 +169,12 @@ public IReactSiteConfiguration SetReuseJavaScriptEngines(bool value)
169169
}
170170

171171
/// <summary>
172-
/// Gets or sets the number of engines to initially start when a pool is created.
172+
/// Gets or sets the number of engines to initially start when a pool is created.
173173
/// Defaults to <c>10</c>.
174174
/// </summary>
175175
public int? StartEngines { get; set; }
176176
/// <summary>
177-
/// Sets the number of engines to initially start when a pool is created.
177+
/// Sets the number of engines to initially start when a pool is created.
178178
/// Defaults to <c>10</c>.
179179
/// </summary>
180180
public IReactSiteConfiguration SetStartEngines(int? startEngines)
@@ -184,12 +184,12 @@ public IReactSiteConfiguration SetStartEngines(int? startEngines)
184184
}
185185

186186
/// <summary>
187-
/// Gets or sets the maximum number of engines that will be created in the pool.
187+
/// Gets or sets the maximum number of engines that will be created in the pool.
188188
/// Defaults to <c>25</c>.
189189
/// </summary>
190190
public int? MaxEngines { get; set; }
191191
/// <summary>
192-
/// Sets the maximum number of engines that will be created in the pool.
192+
/// Sets the maximum number of engines that will be created in the pool.
193193
/// Defaults to <c>25</c>.
194194
/// </summary>
195195
public IReactSiteConfiguration SetMaxEngines(int? maxEngines)
@@ -237,7 +237,7 @@ public IReactSiteConfiguration SetAllowJavaScriptPrecompilation(bool allowJavaSc
237237
public bool LoadReact { get; set; }
238238

239239
/// <summary>
240-
/// Sets whether the built-in version of React is loaded. If <c>false</c>, you must
240+
/// Sets whether the built-in version of React is loaded. If <c>false</c>, you must
241241
/// provide your own version of React.
242242
/// </summary>
243243
/// <returns>The configuration, for chaining</returns>
@@ -332,7 +332,7 @@ public IReactSiteConfiguration DisableServerSideRendering()
332332
public Action<Exception, string, string> ExceptionHandler { get; set; }
333333

334334
/// <summary>
335-
///
335+
///
336336
/// </summary>
337337
/// <param name="handler"></param>
338338
/// <returns></returns>
@@ -343,13 +343,13 @@ public IReactSiteConfiguration SetExceptionHandler(Action<Exception, string, str
343343
}
344344

345345
/// <summary>
346-
/// A provider that returns a nonce to be used on any script tags on the page.
346+
/// A provider that returns a nonce to be used on any script tags on the page.
347347
/// This value must match the nonce used in the Content Security Policy header on the response.
348348
/// </summary>
349349
public Func<string> ScriptNonceProvider { get; set; }
350350

351351
/// <summary>
352-
/// Sets a provider that returns a nonce to be used on any script tags on the page.
352+
/// Sets a provider that returns a nonce to be used on any script tags on the page.
353353
/// This value must match the nonce used in the Content Security Policy header on the response.
354354
/// </summary>
355355
/// <param name="provider"></param>

Diff for: src/React.Router/ReactRouterComponent.cs

+11-1
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,23 @@ protected override void WriteComponentInitialiser(TextWriter writer)
9393
/// Client side React Router does not need context nor explicit path parameter.
9494
/// </summary>
9595
/// <returns>JavaScript</returns>
96-
public override void RenderJavaScript(TextWriter writer)
96+
public override void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad)
9797
{
98+
if (waitForDOMContentLoad)
99+
{
100+
writer.Write("window.addEventListener('DOMContentLoaded', function() {");
101+
}
102+
98103
writer.Write("ReactDOM.hydrate(");
99104
base.WriteComponentInitialiser(writer);
100105
writer.Write(", document.getElementById(\"");
101106
writer.Write(ContainerId);
102107
writer.Write("\"))");
108+
109+
if (waitForDOMContentLoad)
110+
{
111+
writer.Write("});");
112+
}
103113
}
104114
}
105115
}

0 commit comments

Comments
 (0)