Skip to content

Commit d614477

Browse files
Add server-render support for CSS-in-JS (reactjs#589)
* Add callbacks for running custom Javascript during component render This allows UI libraries like styled-components to work without any extra code in this project See also reactjs#487 reactjs#538 * Add babel integration test * Add demo using styled-components * Create abstraction for chaining pre, during, and post render callbacks * Add dummy comments to fix build (address these later) * Remove obsolete method reference * Fix tests * Cleanup some code around RenderFunctions * Expose string property instead of closed-over func with side effects * Fix regression in TransformRender * Add render functions tests * Update sample This still needs to be better organized.. * Fix filename * Fix build * Update xml comments
1 parent e644844 commit d614477

File tree

17 files changed

+381
-57
lines changed

17 files changed

+381
-57
lines changed

src/React.AspNet/HtmlHelperExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ private static IReactEnvironment Environment
5353
/// <param name="serverOnly">Skip rendering React specific data-attributes, container and client-side initialisation during server side rendering. Defaults to <c>false</c></param>
5454
/// <param name="containerClass">HTML class(es) to set on the container tag</param>
5555
/// <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>
56+
/// <param name="renderFunctions">Functions to call during component render</param>
5657
/// <returns>The component's HTML</returns>
5758
public static IHtmlString React<T>(
5859
this IHtmlHelper htmlHelper,
@@ -63,7 +64,8 @@ public static IHtmlString React<T>(
6364
bool clientOnly = false,
6465
bool serverOnly = false,
6566
string containerClass = null,
66-
Action<Exception, string, string> exceptionHandler = null
67+
Action<Exception, string, string> exceptionHandler = null,
68+
RenderFunctions renderFunctions = null
6769
)
6870
{
6971
return new ActionHtmlString(writer =>
@@ -81,7 +83,7 @@ public static IHtmlString React<T>(
8183
reactComponent.ContainerClass = containerClass;
8284
}
8385

84-
reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler);
86+
reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler, renderFunctions);
8587
}
8688
finally
8789
{

src/React.Core/IReactComponent.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ public interface IReactComponent
5454
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
5555
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
5656
/// <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>
57+
/// <param name="renderFunctions">Functions to call during component render</param>
5758
/// <returns>HTML</returns>
58-
string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null);
59+
string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null, RenderFunctions renderFunctions = null);
5960

6061
/// <summary>
6162
/// Renders the JavaScript required to initialise this component client-side. This will
@@ -73,8 +74,9 @@ public interface IReactComponent
7374
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
7475
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
7576
/// <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>
77+
/// <param name="renderFunctions">Functions to call during component render</param>
7678
/// <returns>HTML</returns>
77-
void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null);
79+
void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null, RenderFunctions renderFunctions = null);
7880

7981
/// <summary>
8082
/// Renders the JavaScript required to initialise this component client-side. This will

src/React.Core/ReactComponent.cs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,11 @@ public ReactComponent(IReactEnvironment environment, IReactSiteConfiguration con
125125
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
126126
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
127127
/// <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>
128+
/// <param name="renderFunctions">Functions to call during component render</param>
128129
/// <returns>HTML</returns>
129-
public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
130+
public virtual string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null, RenderFunctions renderFunctions = null)
130131
{
131-
using (var writer = new StringWriter())
132-
{
133-
RenderHtml(writer, renderContainerOnly, renderServerOnly, exceptionHandler);
134-
return writer.ToString();
135-
}
132+
return GetStringFromWriter(renderHtmlWriter => RenderHtml(renderHtmlWriter, renderContainerOnly, renderServerOnly, exceptionHandler, renderFunctions));
136133
}
137134

138135
/// <summary>
@@ -143,8 +140,9 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
143140
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering.</param>
144141
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
145142
/// <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>
143+
/// <param name="renderFunctions">Functions to call during component render</param>
146144
/// <returns>HTML</returns>
147-
public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null)
145+
public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null, RenderFunctions renderFunctions = null)
148146
{
149147
if (!_configuration.UseServerSideRendering)
150148
{
@@ -172,11 +170,22 @@ public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = fal
172170
try
173171
{
174172
stringWriter.Write(renderServerOnly ? "ReactDOMServer.renderToStaticMarkup(" : "ReactDOMServer.renderToString(");
175-
WriteComponentInitialiser(stringWriter);
173+
if (renderFunctions != null)
174+
{
175+
stringWriter.Write(renderFunctions.TransformRender(GetStringFromWriter(componentInitWriter => WriteComponentInitialiser(componentInitWriter))));
176+
}
177+
else
178+
{
179+
WriteComponentInitialiser(stringWriter);
180+
}
176181
stringWriter.Write(')');
177182

183+
renderFunctions?.PreRender(x => _environment.Execute<string>(x));
184+
178185
html = _environment.Execute<string>(stringWriter.ToString());
179186

187+
renderFunctions?.PostRender(x => _environment.Execute<string>(x));
188+
180189
if (renderServerOnly)
181190
{
182191
writer.Write(html);
@@ -221,11 +230,7 @@ public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = fal
221230
/// <returns>JavaScript</returns>
222231
public virtual string RenderJavaScript()
223232
{
224-
using (var writer = new StringWriter())
225-
{
226-
RenderJavaScript(writer);
227-
return writer.ToString();
228-
}
233+
return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter));
229234
}
230235

231236
/// <summary>
@@ -289,5 +294,14 @@ internal static void EnsureComponentNameValid(string componentName)
289294
throw new ReactInvalidComponentException($"Invalid component name '{componentName}'");
290295
}
291296
}
297+
298+
private string GetStringFromWriter(Action<TextWriter> fnWithTextWriter)
299+
{
300+
using (var textWriter = new StringWriter())
301+
{
302+
fnWithTextWriter(textWriter);
303+
return textWriter.ToString();
304+
}
305+
}
292306
}
293307
}

src/React.Core/RenderFunctions.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
3+
namespace React
4+
{
5+
/// <summary>
6+
/// Functions to execute during a render request.
7+
/// These functions will share the same Javascript context, so state can be passed around via variables.
8+
/// </summary>
9+
public abstract class RenderFunctions
10+
{
11+
private readonly RenderFunctions m_renderFunctions;
12+
13+
/// <summary>
14+
/// Constructor. Supports chained calls to multiple render functions by passing in a set of functions that should be called next.
15+
/// The functions within the provided RenderFunctions will be called *after* this instance's.
16+
/// Supports null as an argument.
17+
/// </summary>
18+
/// <param name="renderFunctions">The chained render functions to call</param>
19+
protected RenderFunctions(RenderFunctions renderFunctions)
20+
{
21+
m_renderFunctions = renderFunctions;
22+
}
23+
24+
/// <summary>
25+
/// Implementation of PreRender
26+
/// </summary>
27+
/// <param name="executeJs"></param>
28+
protected virtual void PreRenderCore(Func<string, string> executeJs)
29+
{
30+
}
31+
32+
/// <summary>
33+
/// Implementation of TransformRender
34+
/// </summary>
35+
/// <param name="componentToRender"></param>
36+
/// <returns></returns>
37+
protected virtual string TransformRenderCore(string componentToRender)
38+
{
39+
return componentToRender;
40+
}
41+
42+
/// <summary>
43+
/// Implementation of PostRender
44+
/// </summary>
45+
/// <param name="executeJs"></param>
46+
protected virtual void PostRenderCore(Func<string, string> executeJs)
47+
{
48+
}
49+
50+
/// <summary>
51+
/// Executes before component render.
52+
/// It takes a func that accepts a Javascript code expression to evaluate, which returns the result of the expression.
53+
/// This is useful for setting up variables that will be referenced after the render completes.
54+
/// <param name="executeJs">The func to execute</param>
55+
/// </summary>
56+
public virtual void PreRender(Func<string, string> executeJs)
57+
{
58+
PreRenderCore(executeJs);
59+
m_renderFunctions?.PreRender(executeJs);
60+
}
61+
62+
63+
/// <summary>
64+
/// Transforms the React.createElement expression.
65+
/// This is useful for libraries like styled components which require wrapping the root component
66+
/// inside a helper to generate a stylesheet.
67+
/// Example transform: React.createElement(Foo, ...) => wrapComponent(React.createElement(Foo, ...))
68+
/// </summary>
69+
/// <param name="componentToRender">The Javascript expression to wrap</param>
70+
/// <returns>A wrapped expression</returns>
71+
public string TransformRender(string componentToRender)
72+
{
73+
return m_renderFunctions == null
74+
? TransformRenderCore(componentToRender)
75+
: m_renderFunctions.TransformRender(TransformRenderCore(componentToRender));
76+
}
77+
78+
79+
/// <summary>
80+
/// Executes after component render.
81+
/// It takes a func that accepts a Javascript code expression to evaluate, which returns the result of the expression.
82+
/// This is useful for reading computed state, such as generated stylesheets or a router redirect result.
83+
/// </summary>
84+
/// <param name="executeJs">The func to execute</param>
85+
public void PostRender(Func<string, string> executeJs)
86+
{
87+
PostRenderCore(executeJs);
88+
m_renderFunctions?.PostRender(executeJs);
89+
}
90+
}
91+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
3+
namespace React
4+
{
5+
/// <summary>
6+
/// Render functions for styled components
7+
/// </summary>
8+
public class StyledComponentsFunctions : RenderFunctions
9+
{
10+
/// <summary>
11+
/// Constructor. Supports chained calls to multiple render functions by passing in a set of functions that should be called next.
12+
/// The functions within the provided RenderFunctions will be called *after* this instance's.
13+
/// Supports null as an argument.
14+
/// </summary>
15+
/// <param name="renderFunctions">The chained render functions to call</param>
16+
public StyledComponentsFunctions(RenderFunctions renderFunctions = null)
17+
: base(renderFunctions)
18+
{
19+
}
20+
21+
/// <summary>
22+
/// HTML style tag containing the rendered styles
23+
/// </summary>
24+
public string RenderedStyles { get; private set; }
25+
26+
/// <summary>
27+
/// Implementation of PreRender
28+
/// </summary>
29+
/// <param name="executeJs"></param>
30+
protected override void PreRenderCore(Func<string, string> executeJs)
31+
{
32+
executeJs("var serverStyleSheet = new Styled.ServerStyleSheet();");
33+
}
34+
35+
/// <summary>
36+
/// Implementation of TransformRender
37+
/// </summary>
38+
/// <param name="componentToRender"></param>
39+
/// <returns></returns>
40+
protected override string TransformRenderCore(string componentToRender)
41+
{
42+
return ($"serverStyleSheet.collectStyles({componentToRender})");
43+
}
44+
45+
/// <summary>
46+
/// Implementation of PostRender
47+
/// </summary>
48+
/// <param name="executeJs"></param>
49+
protected override void PostRenderCore(Func<string, string> executeJs)
50+
{
51+
RenderedStyles = executeJs("serverStyleSheet.getStyleTags()");
52+
}
53+
}
54+
}

src/React.Router/HtmlHelperExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public static IHtmlString ReactRouterWithContext<T>(
8484
/// <param name="clientOnly">Skip rendering server-side and only output client-side initialisation code. Defaults to <c>false</c></param>
8585
/// <param name="serverOnly">Skip rendering React specific data-attributes during server side rendering. Defaults to <c>false</c></param>
8686
/// <param name="containerClass">HTML class(es) to set on the container tag</param>
87+
/// <param name="renderFunctions">Functions to call during component render</param>
8788
/// <returns><see cref="IHtmlString"/> containing the rendered markup for provided React Router component</returns>
8889
public static IHtmlString ReactRouter<T>(
8990
this IHtmlHelper htmlHelper,
@@ -95,7 +96,8 @@ public static IHtmlString ReactRouter<T>(
9596
bool clientOnly = false,
9697
bool serverOnly = false,
9798
string containerClass = null,
98-
Action<HttpResponse, RoutingContext> contextHandler = null
99+
Action<HttpResponse, RoutingContext> contextHandler = null,
100+
RenderFunctions renderFunctions = null
99101
)
100102
{
101103
try
@@ -121,7 +123,7 @@ var reactComponent
121123
reactComponent.ContainerClass = containerClass;
122124
}
123125

124-
var executionResult = reactComponent.RenderRouterWithContext(clientOnly, serverOnly);
126+
var executionResult = reactComponent.RenderRouterWithContext(clientOnly, serverOnly, renderFunctions);
125127

126128
if (executionResult.Context?.status != null || executionResult.Context?.url != null)
127129
{

src/React.Router/ReactRouterComponent.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,26 @@ string path
4848
/// </summary>
4949
/// <param name="renderContainerOnly">Only renders component container. Used for client-side only rendering. Does not make sense in this context but included for consistency</param>
5050
/// <param name="renderServerOnly">Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.</param>
51+
/// <param name="renderFunctions">Functions to call during component render</param>
5152
/// <returns>Object containing HTML in string format and the React Router context object</returns>
52-
public virtual ExecutionResult RenderRouterWithContext(bool renderContainerOnly = false, bool renderServerOnly = false)
53+
public virtual ExecutionResult RenderRouterWithContext(
54+
bool renderContainerOnly = false,
55+
bool renderServerOnly = false,
56+
RenderFunctions renderFunctions = null
57+
)
5358
{
54-
_environment.Execute("var context = {};");
59+
var reactRouterFunctions = new ReactRouterFunctions(renderFunctions: renderFunctions);
5560

56-
var html = RenderHtml(renderContainerOnly, renderServerOnly);
57-
58-
var contextString = _environment.Execute<string>("JSON.stringify(context);");
61+
var html = RenderHtml(
62+
renderContainerOnly,
63+
renderServerOnly,
64+
renderFunctions: reactRouterFunctions
65+
);
5966

6067
return new ExecutionResult
6168
{
6269
RenderResult = html,
63-
Context = JsonConvert.DeserializeObject<RoutingContext>(contextString),
70+
Context = JsonConvert.DeserializeObject<RoutingContext>(reactRouterFunctions.ReactRouterContext),
6471
};
6572
}
6673

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
3+
namespace React.Router
4+
{
5+
/// <summary>
6+
/// Render functions for React Router
7+
/// </summary>
8+
public class ReactRouterFunctions : RenderFunctions
9+
{
10+
/// <summary>
11+
/// Constructor. Supports chained calls to multiple render functions by passing in a set of functions that should be called next.
12+
/// The functions within the provided RenderFunctions will be called *after* this instance's.
13+
/// Supports null as an argument.
14+
/// </summary>
15+
/// <param name="renderFunctions">The chained render functions to call</param>
16+
public ReactRouterFunctions(RenderFunctions renderFunctions = null)
17+
: base(renderFunctions)
18+
{
19+
}
20+
21+
/// <summary>
22+
/// The returned react router context, as a JSON string
23+
/// </summary>
24+
public string ReactRouterContext { get; private set; }
25+
26+
/// <summary>
27+
/// Implementation of PreRender
28+
/// </summary>
29+
/// <param name="executeJs"></param>
30+
protected override void PreRenderCore(Func<string, string> executeJs)
31+
{
32+
executeJs("var context = {};");
33+
}
34+
35+
/// <summary>
36+
/// Implementation of PostRender
37+
/// </summary>
38+
/// <param name="executeJs"></param>
39+
protected override void PostRenderCore(Func<string, string> executeJs)
40+
{
41+
ReactRouterContext = executeJs("JSON.stringify(context);");
42+
}
43+
}
44+
}

src/React.Sample.Router.CoreMvc/Content/components/expose-components.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ require('expose-loader?ReactDOM!react-dom');
33
require('expose-loader?ReactDOMServer!react-dom/server');
44

55
require('expose-loader?RootComponent!./home.jsx');
6+
require('expose-loader?Styled!styled-components');

0 commit comments

Comments
 (0)