You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
SSR.Net makes it possible to server side render components in React 17, React 18 and Vue 3. It is open for extension, and can support other frameworks. It aims to be a performant, minimalistic way to support SSR in .Net.
5
+
SSR.Net enables you to Server Side Render (SSR) components in React 17, React 18 and Vue 3 in .Net Framework/Core/5/6/7. It is open for extension and can support other frontend frameworks. It aims at being a performant, minimalistic way to support SSR in .Net.
6
+
6
7
It is based on many of the ideas in ReactJS.Net: https://github.com/reactjs/React.NET.
7
-
This README is not finished yet.
8
+
9
+
**Table of Contents**
10
+
11
+
[TOC]
8
12
9
13
## Installation
10
14
This library can be installed through NuGet. You will also need to install a JavaScriptEngineSwitcher engine and native engine libraries according to your environment.
@@ -21,18 +25,63 @@ The easiest way to get started is to copy or look at one of the example projects
21
25
To build frontend, just run the build scripts in package.json.
22
26
23
27
To configure and use SSR.Net, you need to do several things:
24
-
* Build a JavaScript bundle fit for SSR
25
-
* Set up a web project
26
-
* Set up a controller and a view
28
+
*[Build a JavaScript bundle fit for SSR](#Building-a-JavaScript-bundle-fit-for-SSR)
29
+
*[Set up a web project](#set-up-a-web-project)
30
+
*[Set up a controller and a view](#Set-up-a-controller-and-a-view)
27
31
28
32
We will look at how to do this for React 18 and .Net 6 in the following chapters. There are examples for React 17, React 18 and Vue 3 for both .Net Framework and .Net 6 in the code base. The examples in .Net 6 should be quite descriptive for .Net Core 2 and forward.
29
33
30
34
### Building a JavaScript bundle fit for SSR
31
35
32
-
Create a bundle with some components. For React 18, there is an example here: https://github.com/knowit/SSR.Net/tree/main/src/Frontend/React18Source
36
+
We start by creating a bundle with some components. For React 18, there is an example here: https://github.com/knowit/SSR.Net/tree/main/src/Frontend/React18Source
37
+
38
+
We will create a FrontPage component for this example (source code here: https://github.com/knowit/SSR.Net/blob/main/src/Frontend/React18Source/src/Components/FrontPage.tsx).
Expose the React, ReactDOMServer and ReactDOMClient libraries as global variables. Also expose your components somehow. See an example of how this is done here:
80
+
This component has a simple expand/collapse function. This will show us that React is actually working interactively on the client side.
81
+
82
+
We then create an entry point for our bundle. Here, we expose the React, ReactDOMServer and ReactDOMClient libraries as global variables. We also expose the components we've created. It works well to just import them all and add them to a global Components variable. See an example of how this is done here:
For .Net 6 the resulting js bundle is build to the wwwroot folder as react18example.js: https://github.com/knowit/SSR.Net/tree/main/src/SSR.Net.DotNet6/wwwroot
98
+
In this simple example we use esbuild alone to build the bundle, but it's no problem to use Webpack. You can also use Vite, but you will need to turn off the built in minification and find a different way of minifying it, otherwise the global variables will be removed from the bundle during tree shaking. The esbuild command we use is in the package.json (https://github.com/knowit/SSR.Net/blob/main/src/Frontend/React18Source/package.json):
The resulting js bundle is build to the wwwroot folder in the DotNet6 project as react18example.js: https://github.com/knowit/SSR.Net/tree/main/src/SSR.Net.DotNet6/wwwroot
50
114
51
115
### Set up a web project
52
116
53
-
An example of this is set up here: https://github.com/knowit/SSR.Net/tree/main/src/SSR.Net.DotNet6
117
+
Let's look at this example web project: https://github.com/knowit/SSR.Net/tree/main/src/SSR.Net.DotNet6
54
118
55
-
Create an extension method for adding a React18Renderer to IoC. Here is an example:
119
+
We have an extension method for adding a React18Renderer to IoC, which is the normal way of adding things to .Net Core and forward:
We use the V8 JavaScript engine for this example, and configure a new JavaScriptEnginePool. To make this work, you have to add some packages to your web project. See this file for reference:
143
+
The code above adds both a server side polyfill named React18TextEncoderPolyfill.js (React 18 needs this, and it's missing in the Java Script Engine), and the bundle we created in [Building a JavaScript bundle fit for SSR](#Building-a-JavaScript-bundle-fit-for-SSR), react18example.js. These script files will be run on each server side JavaScript engine during initialization. This means that each JavaScript engine on the server side will be able to run React, ReactDOMServer, ReactDOMClient and access our FrontPage component through the global variable Components.
144
+
145
+
We use the V8 JavaScript engine in this example. This, and the scripts, is configured in a JavaScriptEnginePool. This pool will manage JavaScript engines, create new ones proactively, dispose exhausted engines and be a broker for the actual rendering commands.
146
+
147
+
To make this work, we have to reference some packages in our web project:
Then we add a server side polyfill named React18TextEncoderPolyfill.js, and react18example.js (our bundle, with React 18, the components and the global variables). These script files will be run on each server side JavaScript engine during initialization.
90
-
91
-
After this is done, we call the extension method during the initialization of our program. See this file for reference: https://github.com/knowit/SSR.Net/blob/main/src/SSR.Net.DotNet6/Program.cs
158
+
After this is done, we call the extension method during the initialization of our program in Program.cs: https://github.com/knowit/SSR.Net/blob/main/src/SSR.Net.DotNet6/Program.cs
To do the actual Server Side Rendering, we need to serialize props for the React component we want to Server Side Render and send the props as JSON to the React18Renderer, together with the component name. In this simplified example, we do this directly in the controller (https://github.com/knowit/SSR.Net/blob/main/src/SSR.Net.DotNet6/Controllers/HomeController.cs).
168
+
169
+
First, we inject the React18Renderer in the controller (some lines are removed for brevity and clarity):
public HomeController(React18Renderer react18Renderer)
176
+
{
177
+
_react18Renderer = react18Renderer;
178
+
}
179
+
...
180
+
}
181
+
```
182
+
183
+
Then we add an action to the controller, where we serialize our model and let the React18Renderer create both the HTML and the initialization script. These are returned as a RenderedComponent from the React18Renderer, and passed on to the view:
184
+
185
+
```
186
+
public ActionResult React18() {
187
+
var propsJson = JsonConvert.SerializeObject(
188
+
new {
189
+
header = "React 18 with SSR",
190
+
links = new[]{
191
+
new {
192
+
text = "Google.com",
193
+
href = "https://www.google.com"
194
+
},
195
+
new {
196
+
text = "Hacker news",
197
+
href = "https://news.ycombinator.org"
198
+
}
199
+
}
200
+
});
201
+
var renderedComponent = _react18Renderer.RenderComponent("Components.FrontPage", propsJson);
202
+
return View(renderedComponent);
203
+
}
204
+
```
205
+
206
+
The view is kept very simple (https://github.com/knowit/SSR.Net/blob/main/src/SSR.Net.DotNet6/Views/Home/React18.cshtml):
Here we use RenderedComponent as our model. We set Layout to null, because we only use the markup in this file. We include the react18example.js script that we created in [Building a JavaScript bundle fit for SSR](#Building-a-JavaScript-bundle-fit-for-SSR) in a <script> tag in the <head> tag.
231
+
232
+
Inside the <body> tag, there are just two lines. The first one takes the HTML from our RenderedComponent and renders it as a raw string in the view. The second one takes the initialization script and renders it as a raw string inside a <script> tag.
233
+
234
+
## Running the solution
235
+
236
+
When running the solution and visiting the React 18 view, we see that it's working and that we can interact with it (clicking Expand links will expand the links):
237
+
238
+

239
+
240
+
If we view the source, we see that we get markup from the server and not just an empty <div>. We also see the initialization script, which instructs React to latch on to the DOM in the <div> with the markup by calling ReactDOMClient.hydrateRoot:
241
+
242
+

243
+
244
+
If the rendering was done without SSR, then the div would be empty and the initialization script would call ReactDOMClient.createRoot:
245
+
246
+

247
+
100
248
## Performance and scaling
101
-
In order to be ready to render components quickly, SSR.Net ensures that a configurable number of JavaScript engines are initialized with a given JavaScript bundle. There are several thresholds controlling this:
249
+
In order to be ready to render components quickly, SSR.Net ensures that a configurable number of JavaScript engines are initialized with a given JavaScript bundle. This is done when instantiating a new JavaScriptEnginePool. There are several thresholds controlling this:
102
250
* Minimum engines: When starting or reconfiguring SSR.Net, it will make sure there are at least this number of engines in the engine pool.
103
251
* Maximum engines: The number of engines in the engine pool will never exceed this threshold.
104
252
* Standby engines: The number of unoccupied (spare) engines in the pool.
105
253
* Maximum engine usages: A given engine can be used this number of times before it gets disposed of.
106
254
255
+
There are methods to configure this when initializing the JavaScriptEnginePool:
256
+
257
+
```
258
+
var pool = new JavaScriptEnginePool(new V8JsEngineFactory(), config =>
Let's say we have configured a minimum of 5 engines, a maximum of 25 engines and 3 engines on standby. Before any requests come in, there will be 5 vacant engines.
110
273
Then 3 requests come in at the same time, occupying 3 engines. SSR.Net will see that there are only 2 engines on standby, and will initialize another engine to comply with the "3 engines on standby" requirement.
111
-
Traffic increases, and at some point, 23 engines are occupied concurrently. SSR.Net will ensure that there are 25 engines initialized, but will not exceed this threshold, to comply with the "maximum of 25 engines" requirement.
112
-
Then, there are only 2-3 concurrent requests for a long time. The engines will eventually be exhausted and disposed of, and the number of engines sinks.
113
-
At some point, there are only 5 engines left. The next time an engine gets exhausted, SSR.Net will dispose of it and see that there are only 4 engines left. Then SSR.Net will initialize a new engine to comply with the "minimum of 5 engines" requirement.
114
274
275
+
Traffic increases drastically, and at some point 23 engines are occupied concurrently. SSR.Net will ensure that there are 25 engines initialized, but will not exceed this threshold, to comply with the "maximum of 25 engines" requirement.
115
276
277
+
Then, there are only 2-3 concurrent requests for a long time. The engines will eventually be exhausted and disposed of, and the number of engines declines. At some point, there are only 5 engines left. The next time an engine gets exhausted, SSR.Net will dispose of it and see that there are only 4 engines left. Then SSR.Net will initialize a new engine to comply with the "minimum of 5 engines" requirement.
116
278
117
-
## Limitations when doing SSR:
279
+
## Limitations and workarounds when doing SSR
118
280
* The window object does not exist. This means that your code needs to check if window is defined before using it:
119
281
```
120
282
if (typeof window !== 'undefined')
@@ -123,4 +285,14 @@ if (typeof window !== 'undefined')
123
285
}
124
286
```
125
287
* Global variables must not be used (except the ones described above for exposing objects to SSR.Net). Each JavaScript Engine is used multiple times, so global variables will cause state to be shared between requests, meaning data can leak from one user to another.
126
-
* Some 3rd party packages may fail, if they use the window object or global variables.
288
+
* Some 3rd party packages may fail, if they use the window object or global variables.
289
+
* SSR in React won't run useEffect hooks. So using the window object in a useEffect is safe. Also, if you have code that requires the window object and that doesn't contain markup that you need served from the server side, then you can defer including it to after the first contentful paint:
0 commit comments