├── .gitignore
├── .npmignore
├── App.js
├── LICENSE
├── README.md
├── browser.js
├── package.json
└── server.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | coverage*
4 | .tern-port
5 | v8.log
6 | package-lock.json
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | test
2 | coverage
3 | examples
4 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | var createReactClass = require('create-react-class')
2 | var DOM = require('react-dom-factories')
3 | var div = DOM.div, button = DOM.button, ul = DOM.ul, li = DOM.li
4 |
5 | // This is just a simple example of a component that can be rendered on both
6 | // the server and browser
7 |
8 | module.exports = createReactClass({
9 |
10 | // We initialise its state by using the `props` that were passed in when it
11 | // was first rendered. We also want the button to be disabled until the
12 | // component has fully mounted on the DOM
13 | getInitialState: function() {
14 | return {items: this.props.items, disabled: true}
15 | },
16 |
17 | // Once the component has been mounted, we can enable the button
18 | componentDidMount: function() {
19 | this.setState({disabled: false})
20 | },
21 |
22 | // Then we just update the state whenever its clicked by adding a new item to
23 | // the list - but you could imagine this being updated with the results of
24 | // AJAX calls, etc
25 | handleClick: function() {
26 | this.setState({
27 | items: this.state.items.concat('Item ' + this.state.items.length),
28 | })
29 | },
30 |
31 | // For ease of illustration, we just use the React JS methods directly
32 | // (no JSX compilation needed)
33 | // Note that we allow the button to be disabled initially, and then enable it
34 | // when everything has loaded
35 | render: function() {
36 |
37 | return div(null,
38 |
39 | button({onClick: this.handleClick, disabled: this.state.disabled}, 'Add Item'),
40 |
41 | ul({children: this.state.items.map(function(item) {
42 | return li(null, item)
43 | })})
44 |
45 | )
46 | },
47 | })
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2013 Michael Hart (michael.hart.au@gmail.com)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | react-server-example
2 | --------------------
3 |
4 | A simple (no compile) example of how to do server-side rendering with the
5 | [React](http://facebook.github.io/react/) library so that component code can be
6 | shared between server and browser, as well as getting fast initial page loads
7 | and search-engine-friendly pages.
8 |
9 | A more complex example with shared routing and data fetching can be found at
10 | [react-server-routing-example](https://github.com/mhart/react-server-routing-example).
11 |
12 | Example
13 | -------
14 |
15 | ```sh
16 | $ npm install
17 | $ node server.js
18 | ```
19 |
20 | Then navigate to [http://localhost:3000](http://localhost:3000) and
21 | click on the button to see some reactive events in action.
22 |
23 | Try viewing the page source to ensure the HTML being sent from the server is already rendered
24 | (with checksums to determine whether client-side rendering is necessary)
25 |
26 | Here are the files involved:
27 |
28 | `App.js`:
29 | ```js
30 | var createReactClass = require('create-react-class')
31 | var DOM = require('react-dom-factories')
32 | var div = DOM.div, button = DOM.button, ul = DOM.ul, li = DOM.li
33 |
34 | // This is just a simple example of a component that can be rendered on both
35 | // the server and browser
36 |
37 | module.exports = createReactClass({
38 |
39 | // We initialise its state by using the `props` that were passed in when it
40 | // was first rendered. We also want the button to be disabled until the
41 | // component has fully mounted on the DOM
42 | getInitialState: function() {
43 | return {items: this.props.items, disabled: true}
44 | },
45 |
46 | // Once the component has been mounted, we can enable the button
47 | componentDidMount: function() {
48 | this.setState({disabled: false})
49 | },
50 |
51 | // Then we just update the state whenever its clicked by adding a new item to
52 | // the list - but you could imagine this being updated with the results of
53 | // AJAX calls, etc
54 | handleClick: function() {
55 | this.setState({
56 | items: this.state.items.concat('Item ' + this.state.items.length),
57 | })
58 | },
59 |
60 | // For ease of illustration, we just use the React JS methods directly
61 | // (no JSX compilation needed)
62 | // Note that we allow the button to be disabled initially, and then enable it
63 | // when everything has loaded
64 | render: function() {
65 |
66 | return div(null,
67 |
68 | button({onClick: this.handleClick, disabled: this.state.disabled}, 'Add Item'),
69 |
70 | ul({children: this.state.items.map(function(item) {
71 | return li(null, item)
72 | })})
73 |
74 | )
75 | },
76 | })
77 | ```
78 |
79 | `browser.js`:
80 | ```js
81 | var React = require('react')
82 | var ReactDOM = require('react-dom')
83 | // This is our React component, shared by server and browser thanks to browserify
84 | var App = React.createFactory(require('./App'))
85 |
86 | // This script will run in the browser and will render our component using the
87 | // value from APP_PROPS that we generate inline in the page's html on the server.
88 | // If these props match what is used in the server render, React will see that
89 | // it doesn't need to generate any DOM and the page will load faster
90 |
91 | ReactDOM.render(App(window.APP_PROPS), document.getElementById('content'))
92 | ```
93 |
94 | `server.js`:
95 | ```js
96 | var http = require('http')
97 | var browserify = require('browserify')
98 | var literalify = require('literalify')
99 | var React = require('react')
100 | var ReactDOMServer = require('react-dom/server')
101 | var DOM = require('react-dom-factories')
102 | var body = DOM.body, div = DOM.div, script = DOM.script
103 | // This is our React component, shared by server and browser thanks to browserify
104 | var App = React.createFactory(require('./App'))
105 |
106 | // A variable to store our JS, which we create when /bundle.js is first requested
107 | var BUNDLE = null
108 |
109 | // Just create a plain old HTTP server that responds to two endpoints ('/' and
110 | // '/bundle.js') This would obviously work similarly with any higher level
111 | // library (Express, etc)
112 | http.createServer(function(req, res) {
113 |
114 | // If we hit the homepage, then we want to serve up some HTML - including the
115 | // server-side rendered React component(s), as well as the script tags
116 | // pointing to the client-side code
117 | if (req.url === '/') {
118 |
119 | res.setHeader('Content-Type', 'text/html; charset=utf-8')
120 |
121 | // `props` represents the data to be passed in to the React component for
122 | // rendering - just as you would pass data, or expose variables in
123 | // templates such as Jade or Handlebars. We just use some dummy data
124 | // here (with some potentially dangerous values for testing), but you could
125 | // imagine this would be objects typically fetched async from a DB,
126 | // filesystem or API, depending on the logged-in user, etc.
127 | var props = {
128 | items: [
129 | 'Item 0',
130 | 'Item 1',
131 | 'Item \u2028',
132 | 'Item \u2029',
133 | ],
134 | }
135 |
136 | // Here we're using React to render the outer body, so we just use the
137 | // simpler renderToStaticMarkup function, but you could use any templating
138 | // language (or just a string) for the outer page template
139 | var html = ReactDOMServer.renderToStaticMarkup(body(null,
140 |
141 | // The actual server-side rendering of our component occurs here, and we
142 | // pass our data in as `props`. This div is the same one that the client
143 | // will "render" into on the browser from browser.js
144 | div({
145 | id: 'content',
146 | dangerouslySetInnerHTML: {__html: ReactDOMServer.renderToString(App(props))},
147 | }),
148 |
149 | // The props should match on the client and server, so we stringify them
150 | // on the page to be available for access by the code run in browser.js
151 | // You could use any var name here as long as it's unique
152 | script({
153 | dangerouslySetInnerHTML: {__html: 'var APP_PROPS = ' + safeStringify(props) + ';'},
154 | }),
155 |
156 | // We'll load React from a CDN - you don't have to do this,
157 | // you can bundle it up or serve it locally if you like
158 | script({src: 'https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js'}),
159 | script({src: 'https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js'}),
160 | script({src: 'https://cdn.jsdelivr.net/npm/react-dom-factories@1.0.2/index.min.js'}),
161 | script({src: 'https://cdn.jsdelivr.net/npm/create-react-class@15.6.3/create-react-class.min.js'}),
162 |
163 | // Then the browser will fetch and run the browserified bundle consisting
164 | // of browser.js and all its dependencies.
165 | // We serve this from the endpoint a few lines down.
166 | script({src: '/bundle.js'})
167 | ))
168 |
169 | // Return the page to the browser
170 | res.end(html)
171 |
172 | // This endpoint is hit when the browser is requesting bundle.js from the page above
173 | } else if (req.url === '/bundle.js') {
174 |
175 | res.setHeader('Content-Type', 'text/javascript')
176 |
177 | // If we've already bundled, send the cached result
178 | if (BUNDLE != null) {
179 | return res.end(BUNDLE)
180 | }
181 |
182 | // Otherwise, invoke browserify to package up browser.js and everything it requires.
183 | // We also use literalify to transform our `require` statements for React
184 | // so that it uses the global variable (from the CDN JS file) instead of
185 | // bundling it up with everything else
186 | browserify()
187 | .add('./browser.js')
188 | .transform(literalify.configure({
189 | 'react': 'window.React',
190 | 'react-dom': 'window.ReactDOM',
191 | 'react-dom-factories': 'window.ReactDOMFactories',
192 | 'create-react-class': 'window.createReactClass',
193 | }))
194 | .bundle(function(err, buf) {
195 | // Now we can cache the result and serve this up each time
196 | BUNDLE = buf
197 | res.statusCode = err ? 500 : 200
198 | res.end(err ? err.message : BUNDLE)
199 | })
200 |
201 | // Return 404 for all other requests
202 | } else {
203 | res.statusCode = 404
204 | res.end()
205 | }
206 |
207 | // The http server listens on port 3000
208 | }).listen(3000, function(err) {
209 | if (err) throw err
210 | console.log('Listening on 3000...')
211 | })
212 |
213 |
214 | // A utility function to safely escape JSON for embedding in a \u2028',
37 | 'Item \u2029',
38 | ],
39 | }
40 |
41 | // Here we're using React to render the outer body, so we just use the
42 | // simpler renderToStaticMarkup function, but you could use any templating
43 | // language (or just a string) for the outer page template
44 | var html = ReactDOMServer.renderToStaticMarkup(body(null,
45 |
46 | // The actual server-side rendering of our component occurs here, and we
47 | // pass our data in as `props`. This div is the same one that the client
48 | // will "render" into on the browser from browser.js
49 | div({
50 | id: 'content',
51 | dangerouslySetInnerHTML: {__html: ReactDOMServer.renderToString(App(props))},
52 | }),
53 |
54 | // The props should match on the client and server, so we stringify them
55 | // on the page to be available for access by the code run in browser.js
56 | // You could use any var name here as long as it's unique
57 | script({
58 | dangerouslySetInnerHTML: {__html: 'var APP_PROPS = ' + safeStringify(props) + ';'},
59 | }),
60 |
61 | // We'll load React from a CDN - you don't have to do this,
62 | // you can bundle it up or serve it locally if you like
63 | script({src: 'https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js'}),
64 | script({src: 'https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js'}),
65 | script({src: 'https://cdn.jsdelivr.net/npm/react-dom-factories@1.0.2/index.min.js'}),
66 | script({src: 'https://cdn.jsdelivr.net/npm/create-react-class@15.6.3/create-react-class.min.js'}),
67 |
68 | // Then the browser will fetch and run the browserified bundle consisting
69 | // of browser.js and all its dependencies.
70 | // We serve this from the endpoint a few lines down.
71 | script({src: '/bundle.js'})
72 | ))
73 |
74 | // Return the page to the browser
75 | res.end(html)
76 |
77 | // This endpoint is hit when the browser is requesting bundle.js from the page above
78 | } else if (req.url === '/bundle.js') {
79 |
80 | res.setHeader('Content-Type', 'text/javascript')
81 |
82 | // If we've already bundled, send the cached result
83 | if (BUNDLE != null) {
84 | return res.end(BUNDLE)
85 | }
86 |
87 | // Otherwise, invoke browserify to package up browser.js and everything it requires.
88 | // We also use literalify to transform our `require` statements for React
89 | // so that it uses the global variable (from the CDN JS file) instead of
90 | // bundling it up with everything else
91 | browserify()
92 | .add('./browser.js')
93 | .transform(literalify.configure({
94 | 'react': 'window.React',
95 | 'react-dom': 'window.ReactDOM',
96 | 'react-dom-factories': 'window.ReactDOMFactories',
97 | 'create-react-class': 'window.createReactClass',
98 | }))
99 | .bundle(function(err, buf) {
100 | // Now we can cache the result and serve this up each time
101 | BUNDLE = buf
102 | res.statusCode = err ? 500 : 200
103 | res.end(err ? err.message : BUNDLE)
104 | })
105 |
106 | // Return 404 for all other requests
107 | } else {
108 | res.statusCode = 404
109 | res.end()
110 | }
111 |
112 | // The http server listens on port 3000
113 | }).listen(3000, function(err) {
114 | if (err) throw err
115 | console.log('Listening on 3000...')
116 | })
117 |
118 |
119 | // A utility function to safely escape JSON for embedding in a