├── .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