Skip to content

Commit 1a21378

Browse files
committed
Add tutorial for server-side rendering
1 parent aa0f670 commit 1a21378

File tree

5 files changed

+226
-13
lines changed

5 files changed

+226
-13
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
Fable bindings and helpers for React projects
44

5+
## Documents
6+
7+
### [Server-Side Rendering](docs/server-side-rendering.md):
8+
9+
A **Pure F#** solution for Server-Side Rendering, **No NodeJS Required!**
10+
11+
12+
513
## Why does this repository include bindings for React JS libraries?
614

715
Fable bindings for JS libraries maintained by the Fable team are in [fable-import](https://github.com/fable-compiler/fable-import). However, that repository only contains _pure bindings_ (which only have metadata and thus can be distributed just in `.dll` form), while libraries like `Fable.ReactLeaflet` contain actual code that must be compiled to JS, so they need to include also the sources in the distribution.

Samples/SSRSample/build.fsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ Target "Run" (fun () ->
6262
run dotnetCli "fable webpack-dev-server" clientPath
6363
}
6464
let browser = async {
65-
Threading.Thread.Sleep 5000
66-
Diagnostics.Process.Start "http://localhost:8080" |> ignore
65+
Threading.Thread.Sleep 10000
66+
Diagnostics.Process.Start "http://localhost:8085" |> ignore
6767
}
6868

6969
[ server; client; browser]

Samples/SSRSample/src/Client/View.fs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,6 @@ open Fable.Helpers.React.Props
1111
open Client.Types
1212
open Shared
1313

14-
type [<Pojo>] JsCompProps = {
15-
text: string
16-
}
17-
18-
#if FABLE_COMPILER
19-
let JsComp: React.ComponentClass<JsCompProps> = importDefault "./jsComp"
20-
#else
21-
let JsComp = Unchecked.defaultof<React.ComponentClass<JsCompProps>>
22-
#endif
2314

2415
let show = function
2516
| Some x -> string x
@@ -46,12 +37,17 @@ let safeComponents =
4637
str " powered by: "
4738
components ]
4839

40+
type [<Pojo>] JsCompProps = {
41+
text: string
42+
}
43+
4944
let jsComp (props: JsCompProps) =
5045
ofImport "default" "./jsComp" props []
5146

5247
let jsCompServer (props: JsCompProps) =
5348
div [] [ str "loading" ]
5449

50+
5551
type [<Pojo>] MyProp = {
5652
text: string
5753
}
@@ -181,5 +177,4 @@ let view (model: Model) (dispatch) =
181177
ofFunction fnComp { text = "I'm rendered by Function Component!"} []
182178
ofFunction fnCompWithChildren { text = "I'm rendered by Function Component!"; children=[||]} [ span [] [ str "I'm rendered by children!"] ]
183179
]
184-
185180
]

docs/server-side-rendering.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Five Step to Enable Server-Side Rendering in your Elmish + Dotnet App!
2+
3+
[SSR Sample App of safe-stack template is awailable!](https://github.com/fable-compiler/fable-react/tree/master/Samples/SSRSample)
4+
5+
## Step 1: Reorganize your source files
6+
7+
Separate all your elmish view and types to standalone files, like this:
8+
9+
```F#
10+
pages
11+
|-- Home
12+
|-- View.fs // contains view function.
13+
|-- Types.fs // contains msg and model type definitions, also should include init function.
14+
|-- State.fs // contains update function
15+
16+
```
17+
18+
View.fs and Types.fs will be shared between client and server.
19+
20+
## Step 2. Make shared files can be executed on the server side
21+
22+
Some code works in Fable might throw a run time exception when executed on dotnet, so we should be careful with unsafe type casting and add compiler directives to remove some code if necessary.
23+
24+
Here are some hints about doing this:
25+
26+
### 1. Replace unsafe cast (unbox and `!!`) in your html attributes and css props with `HTMLAttr.Custom`, `SVGAttr.Custom` and `CSSProp.Custom`
27+
28+
```diff
29+
- div [ !!("class", "container") ] []
30+
+ div [ HTMLAttr.Custom ("class", "container") ]
31+
32+
33+
- div [ Style [ !!("class", "container") ] ] []
34+
+ div [ Style [ CSSProp.Custom("class", "container") ] ] []
35+
36+
- svg [ !!("width", 100) ] []
37+
+ svg [ SVGAttr.Custom("class", "container") ] []
38+
```
39+
40+
41+
### 2. Make sure your browser/js code won't be executed on the server side
42+
43+
One big challenge of sharing code between client and sever is that server side has different API environment with client side. In this respect Fable + dotnet's SSR is has not much difference with nodejs, except in dotnet you should not only prevent browser's API call, but also js.
44+
45+
Thanks for Fable Compiler's `FABLE_COMPILER` directive, we can easly distinguish client environment and server environment and execute different code in each environment:
46+
47+
```#F
48+
#if FABLE_COMPILER
49+
executeOnClient ()
50+
#else
51+
executeOnServer ()
52+
#endif
53+
```
54+
55+
We also provice a help function in `Fable.Helpers.Isomorphic` of this, the definition is:
56+
57+
```F#
58+
let inline isomorphicExec clientFn serverFn input =
59+
#if FABLE_COMPILER
60+
clientFn input
61+
#else
62+
serverFn input
63+
#endif
64+
```
65+
66+
Full example:
67+
68+
```diff
69+
open Fable.Core
70+
open Fable.Import.JS
71+
open Fable.Helpers.Isomorphic
72+
open Fable.Import.Browser
73+
74+
// example code to make your document's title has marquee effect
75+
-window.setInterval(
76+
- fun () ->
77+
- document.title <- document.title.[1..len - 1] + document.title.[0..0],
78+
- 600
79+
-)
80+
81+
82+
+let inline clientFn () =
83+
+ window.setInterval(
84+
+ fun () ->
85+
+ document.title <- document.title.[1..len - 1] + document.title.[0..0],
86+
+ 600
87+
+ )
88+
+isomorphicExec clientFn ignore ()
89+
```
90+
91+
92+
### 3. Add placeholder for components cannot been rendered on the serve side, like js native components.
93+
94+
In `Fable.Helpers.Isomorphic` we also implemented a help funciton to render a placeholder element for components that cannot been rendered on the server side, this function will also help [React.hydrate](https://reactjs.org/docs/react-dom.html#hydrate) to understand the differences between htmls rendered by client and server, so React won't treat it as a mistake and warn about it.
95+
96+
```diff
97+
open Fable.Core
98+
open Fable.Import.JS
99+
open Fable.Helpers.React
100+
open Fable.Helpers.Isomorphic
101+
open Fable.Import.Browser
102+
103+
type [<Pojo>] JsCompProps = {
104+
text: string
105+
}
106+
107+
108+
let jsComp (props: JsCompProps) =
109+
ofImport "default" "./jsComp" props []
110+
111+
-jsComp { text="I'm rendered by a js Component!" }
112+
113+
+let jsCompServer (props: JsCompProps) =
114+
+ div [] [ str "loading" ]
115+
+
116+
+isomorphicView jsComp jsCompServer { text="I'm rendered by a js Component!" }
117+
```
118+
119+
## Step 3. Create your init state on the server side.
120+
121+
On the server side, you could create routes like normal MVC app, just make sure the model passed to server rendering function is exactly match the model on the client side in current route.
122+
123+
Here is an example:
124+
125+
```F#
126+
127+
open Giraffe
128+
open Giraffe.GiraffeViewEngine
129+
open FableJson
130+
131+
let initState: Model = {
132+
counter = Some 42
133+
someString = "Some String"
134+
someFloat = 11.11
135+
someInt = 22
136+
}
137+
138+
let renderHtml () =
139+
// This would render the html by model create on the server side.
140+
// Note in an Elmish app, view function takes two parameters,
141+
// the first is model, and the second is dispatch,
142+
// which simple ignored here because React will bind event handlers for you on the client side.
143+
let htmlStr = Fable.Helpers.ReactServer.renderToString(Client.View.view initState ignore)
144+
145+
// We also need to pass the model to Elmish and React by print a json string in html to let them know what's the model that used to rendering the html.
146+
// Note we call ofJson twice here,
147+
// because Elmish's model can contains some complicate type instead of pojo,
148+
// the first one will seriallize the state to json string,
149+
// and the second one will seriallize the json string to a legally js string,
150+
// so we can deseriallize it by Fable's ofJson and get the correct types.
151+
let stateJsonStr = toJson (toJson initState)
152+
153+
html []
154+
[ head [] []
155+
body []
156+
[ div [_id "elmish-app"] [ rawText htmlStr ]
157+
script []
158+
[ rawText (sprintf """
159+
var __INIT_STATE__ = %s
160+
""" stateJsonStr) ] //
161+
script [ _src (assetsBaseUrl + "/public/bundle.js") ] []
162+
]
163+
]
164+
```
165+
166+
## Step 4. Update your elmish app's init function
167+
168+
1. Init your elmish app by state printed in the html.
169+
2. Remove init commands that still fetch data which already printed in the html.
170+
171+
e.g.
172+
173+
```F#
174+
let init () =
175+
// Init model by server side state
176+
let model = ofJson<Model> !!window?__INIT_STATE__
177+
// let cmd =
178+
// Cmd.ofPromise
179+
// (fetchAs<int> "/api/init")
180+
// []
181+
// (Ok >> Init)
182+
// (Error >> Init)
183+
model, Cmd.none
184+
```
185+
186+
## Step 5. Using React.hydrate to render your app
187+
188+
```diff
189+
Program.mkProgram init update view
190+
#if DEBUG
191+
|> Program.withConsoleTrace
192+
|> Program.withHMR
193+
#endif
194+
-|> Program.withReact "elmish-app"
195+
+|> Program.withReactHydrate "elmish-app"
196+
#if DEBUG
197+
|> Program.withDebugger
198+
#endif
199+
|> Program.run
200+
```
201+
202+
Now enjoy! If you find bugs or just need some help, please create an issue and let we know.
203+
204+
## Try the sample app
205+
206+
```sh
207+
git clone https://github.com/fable-compiler/fable-react.git
208+
cd ./fable-react/Samples/SSRSample/
209+
./build.sh # or ./build.cmd in windows
210+
```

src/Fable.React/Fable.Helpers.Isomorphic.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ open Components
3232
open Fable.Helpers.React
3333

3434
/// Isomorphic helper function for conditional executaion
35-
/// it will execute `clientFn model` in the client side and `serverFn model` in the server side
35+
/// it will execute `clientFn model` on the client side and `serverFn model` on the server side
3636
let inline isomorphicExec clientFn serverFn model =
3737
ServerRenderingInternal.isomorphicExec clientFn serverFn model
3838

0 commit comments

Comments
 (0)