├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── gulpfile.babel.js
├── package.json
├── src
├── ReactServer.js
├── Response.js
├── Route.js
├── handlers
│ └── simpleResponse.js
├── index.js
├── middleware
│ ├── Favicon.js
│ ├── Middleware.js
│ ├── Static.js
│ └── index.js
├── server
│ ├── createServer.js
│ ├── index.js
│ ├── runServer.js
│ └── serve.js
└── utils
│ ├── Html.js
│ ├── ReactResponseGreeter.js
│ └── createTemplateString.js
└── test
├── Middleware.spec.js
├── ReactServer.spec.js
├── Response.spec.js
├── consumer
├── consumerenv.js
├── helpers
│ └── favicon.ico
├── routes.js
└── test.js
├── createServer.spec.js
├── helpers
├── Null.js
└── testenv.js
└── runServer.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["add-module-exports", "babel-root-import"],
3 | "presets": ["react", "es2015", "stage-0"]
4 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "plugins": [
5 | "react"
6 | ],
7 | "env": {
8 | "browser": false,
9 | "es6": true,
10 | "node": true
11 | },
12 | "ecmaFeatures": {
13 | "jsx": true,
14 | "classes": true,
15 | "modules": true
16 | },
17 | "rules": {
18 | "no-debugger": 0,
19 | "no-console": 0,
20 | "eqeqeq": 1,
21 | "space-after-keywords": 0,
22 | "max-len": 0,
23 | "indent": [2, 4],
24 | "padded-blocks": 0,
25 | "comma-dangle": 0,
26 | "key-spacing": 0,
27 | "new-cap": 0,
28 | "strict": [2, "global"],
29 | "no-underscore-dangle": 0,
30 | "no-use-before-define": 0,
31 | "eol-last": 0,
32 | "quotes": 0,
33 | "semi": [2, "never"],
34 | "space-infix-ops": 0,
35 | "space-in-parens": 0,
36 | "no-trailing-spaces": 0,
37 | "no-multi-spaces": 0,
38 | "no-alert": 0,
39 | "no-undef": 2,
40 | "func-names": 0,
41 | "space-before-function-paren": [2, "never"],
42 | "no-unused-vars": 0,
43 | "react/jsx-boolean-value": 1,
44 | "react/jsx-curly-spacing": 0,
45 | "react/jsx-indent-props": [2, 4],
46 | "react/prefer-es6-class": [1, "always"],
47 | "react/jsx-uses-react": 1,
48 | "react/jsx-uses-vars": 1,
49 | "react/jsx-no-bind": 0,
50 | "react/jsx-closing-bracket-location": 0
51 | },
52 | "globals" : {
53 | "document": false,
54 | "escape": false,
55 | "navigator": false,
56 | "unescape": false,
57 | "window": false,
58 | "describe": true,
59 | "before": true,
60 | "it": true,
61 | "expect": true,
62 | "sinon": true
63 | }
64 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | *.tgz
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | test
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Daniel Dunderfelt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | React-response
2 | ===
3 |
4 | React-response provides an easy-to-use server-side rendering server for React.
5 | The goal of this project is to reduce the boilerplate you need for a universal React project. Instead of copy-pasting a server from somewhere, React-response makes it possible to quickly write a production-ready server-side React renderer yourself. This will enable you to focus on your app without worrying about how to implement server-side rendering.
6 |
7 | The configuration of your React-response is done with familiar React components, kind of like React Router's route configuration. Almost all of the props have sensible defaults, so for the simplest apps you really don't have to do a lot to get a server running.
8 |
9 | > Production-ready stability is one of my end goals but we're still in the early days. Use in production at your own risk! If you do, do not hesitate to contact me with your experience.
10 |
11 | What's it look like?
12 | ---
13 |
14 | Glad you asked. The simplest hello World with React-response looks like this:
15 |
16 | ```javascript
17 | import { ReactServer, Response, serve, createServer } from 'react-response'
18 |
19 | const server = createServer(
20 |
21 |
22 |
23 | )
24 |
25 | serve(server)
26 |
27 | ```
28 |
29 | Running that will display a built-in Hello World page in your browser at `localhost:3000`.
30 |
31 | The `ReactServer` component instantiates a new Express server. The `Response` component is responsible for rendering a React application at the specified Express route. The simplest example above demonstrates the built-in defaults, but most of React-response's behaviour is customizable. The full example illustrates all the ways you can customize React-response.
32 |
33 | ### The full example
34 |
35 | ```javascript
36 | import { RouterContext } from 'react-router' // RouterContext is your root component if you're using React-router.
37 | import routes from './routes' // React-router routes
38 | import Html from './helpers/Html' // Your template component
39 | import ReactResponse from 'react-response'
40 |
41 | // Install the React-router-response if you use React-router
42 | import createReactRouterResponse from 'react-response-router'
43 |
44 | // Import all the things
45 | const {
46 | ReactServer,
47 | Route,
48 | Response,
49 | serve,
50 | createServer,
51 | Middleware,
52 | Static,
53 | Favicon,
54 | } = ReactResponse
55 |
56 | /* Note that you need to install 'serve-favicon' and other middleware if you want to use them. */
57 |
58 | const server = createServer(
59 | // Set basic server config on the ReactServer component.
60 |
61 | // This is an Express route with the default props. Middlewares need to be
62 | // mounted inside a Route component.
63 |
64 | // React-response ships with wrappers for some commonly used middleware.
65 |
66 |
67 |
68 |
69 | // Set your template and handler.
70 | // React-response uses simple built-in templates and handlers by default.
71 |
72 | // Pass the React component you want to render OR
73 | // a custom render function as a child to Response.
74 |
75 | {(renderProps, req, res) => {
76 |
77 | // Return a map of props for the template component. The Html component
78 | // takes one prop: `component` which should be a rendered React component.
79 | return { component: ReactDOM.renderToString(
80 |
81 | ) }
82 | }}
83 |
84 |
85 | // Many routes
86 |
87 | { /* Implement your APi proxy */ }} />
88 |
89 |
90 | )
91 |
92 | serve(server)
93 |
94 | ```
95 |
96 | Alright, this is more like it! As you can see, with React-response we attach middleware and app renderers to Routes, denoted by the `` component. This is, as we saw in the simple example, completely optional.
97 |
98 | Express middleware is painless to use through the `` component. The middleware will be mounted on the route which the middleware component is a child of. Simply pass in a middleware function as the `use` prop. `Favicon` and `Static` middleware components ship with React-response. They are simple wrappers for the generic middleware component.
99 |
100 | The `` component is where all the action happens. It receives your template component as a prop and the thing you want to render as a child. If you simply pass your app root component as a child to Response, Response will automatically render it to a string with ReactDOM. If you pass a function instead, it will be called with some props from the handler, as well as the request and response data from Express. This is called a custom render function.
101 |
102 | The return value from your custom render function should be a map of props that will be applied to your template component. This is important!
103 |
104 | React-response ships one handler, `simpleResponse`. SimpleResponse will be used by default. Both modules export a factory function which should be called to produce the handler itself. This is your chance to supply additional props to the component that will be rendered! The reactRouterResponse factory expects your router config as its argument which will be used to serve your app. The simpleResponse factory simply splats any object you supply onto the rendered component.
105 |
106 | To illustrate this, an example of the simpleResponse:
107 |
108 | ```javascript
109 |
110 |
111 |
112 | /* EQUALS */
113 |
114 | { renderProps => ({
115 | component: ReactDOM.renderToString()
116 | }) }
117 |
118 | ```
119 |
120 | The custom render function in the above example will receive `{ foo: "bar" }` as the `renderProps` argument. If you simply pass your root component as Response's child, the renderProps will be applied to it.
121 |
122 | This is not very useful in the case of the `simpleResponse`. If you use `reactRouterResponse` (from the `react-response-router` package), you give your route config to the factory and the handler outputs `renderProps` from React-router. An example:
123 |
124 | ```javascript
125 |
126 |
127 |
128 | /* EQUALS */
129 |
130 | { renderProps => ({
131 | component: ReactDOM.renderToString()
132 | }) }
133 |
134 | ```
135 | Note that `` will initially complain about missing props as you start the server if you do not give it the renderProps right away. This is OK and won't hinder the functionality of your app.
136 |
137 | Again, remember to return *a map of props for the template component*. The simple Html skeleton that ships with React-response expects your stringified app as the `component` prop, as illustrated in the examples.
138 |
139 | The whole React-response setup is fed into the `createServer` function. It compiles a fully-featured Express server from the components which you can feed to the `serve` function.
140 |
141 | ### A note on JSX
142 |
143 | I know that some developers are not fond of JSX and prefer vanilla Javascript. My decision to use JSX and even React components for server configuration is bound to raise eyebrows. For me it comes down to preference, usability and how it looks. React-response is all about eliminating React boilerplate and easing the cognitive load of writing a server-side rendering server, so the last thing I wanted was a declarative and messy configuration.
144 |
145 | The very first thing I did with React-response was to design the user interface of the configuration. While the data is not naturally nested like React Router's routes, I feel that using JSX and React components to build the server configuration gives React-response a distinct "React identity". Rendering React components on the server should just be a matter of composing them into the server configuration. It is also very easy to see what is going on from a quick glance at the configuration tree, and in my opinion it is much better than plain Javascript objects.
146 |
147 | However, if you do not wish to use JSX, inspect the output of `createServer`. It should be rather simple to re-create that object structure without JSX. Note that the components, like `Middleware` and `Response`, directly apply middleware and handlers to the Express instance.
148 |
149 | Rest assured that I plan to fully support usage of React-response without React components, à la React Router. It just isn't a priority for the first few releases.
150 |
151 | # Getting started
152 |
153 | First, install React-response:
154 | `npm install --save react-response`
155 |
156 | Make sure you have React-response's peerDependencies (react react-dom express) installed. Also install any middleware you want to use through the components.
157 |
158 | If you are using React-router, install the [React-router response handler](https://github.com/danieldunderfelt/react-response-router) for React-response. This can be achieved with:
159 |
160 | `npm i react-response-router --save`
161 |
162 | The full example above uses it.
163 |
164 | Then, follow the examples above to set up your server config. When done, feed the config to `createServer` and the output of `createServer` into `serve`.
165 |
166 | Before unleashing `node` on your server file, keep in mind that you need Babel to transpile the JSX. I suggest using `babel-core/register` to accomplish this if you do not have transpiling enabled for your server-side code. Like this:
167 |
168 | ```javascript
169 | // server.babel.js
170 | require('babel-core/register')
171 | require('server.js') // Your server file
172 | ```
173 |
174 | Then run that file with Node.
175 |
176 | When run, React-response will output the URL where you can see your app. By default that is `http://localhost:3000`.
177 |
178 | # How do I...
179 |
180 | React-response was never meant to cover 100% of all use cases. I made it according to how I write universal React apps, but I have seen some projects that are simply out of React-response's scope. I am aiming for 80-90% of use cases. That said, React-response is quite accommodating as you can make your own response handlers and rendering functions. Also, many things seen in the `server.js` file of various projects can be moved elsewhere, for example into the root component. The server should only render your app into a template and send the response on its way. The rest can be accomplished elsewhere.
181 |
182 | ### ... use Redux:
183 |
184 | Easily! You need a custom render function where you configure your store, set up the `` and do data fetching. An example:
185 |
186 | ```javascript
187 | // server.js
188 |
189 | { (renderProps, req, res) => {
190 | // If you use react-router-redux, create history for its router state tracking.
191 | const history = createMemoryHistory(req.url)
192 |
193 | // The function that returns your store
194 | const store = configureStore({ initialState: {}, history })
195 |
196 | // You can also use a Root component that composes the Provider and includes your DevTools.
197 | const component = (
198 |
199 |
200 |
201 | )
202 |
203 | // Return props for the template
204 | return {
205 | component,
206 | store
207 | }
208 | }}
209 |
210 |
211 | // YourTemplate.js
212 |
213 | class Html extends Component {
214 |
215 | render() {
216 | const {store, component} = this.props
217 | // First, render your app to a string. See, no need to do even this in server.js!
218 | const content = component ? ReactDOM.renderToString(component) : ''
219 |
220 | return (
221 |
222 |
223 |
224 |
225 |
226 | // Plop in your app
227 |
228 | // Get the state from your store and put it into the template serialized:
229 |
230 |
231 |
232 | )
233 | }
234 | }
235 | ```
236 |
237 | You obviously also need to include your assets in that there Html template, but hopefully this illustrates how we can use Redux and also move some functionality away from the server file itself.
238 |
239 | ### ... create my own response handler?
240 |
241 | Skipping over the othermost function, the factory, The response handler (reactRouterResponse for example) is a function that takes two functions as its arguments. The first one receives the template props from your rendering function and renders the template. It also receives the response object as its second argument. The second one is your custom rendering function, or the component you gave as a child to `` wrapped in the default render function.
242 |
243 | This function should return the route handler that will be attached to the Express route to handle the rendering of your app. Here is the response handler that ships with React-response:
244 |
245 | ```javascript
246 | // 1: response handler factory creator, 2: response handler factory, 3: response handler. SEE?! EASY!
247 | export const createSimpleResponse = (renderProps = {}) => (renderTemplate, renderApp) => (req, res) => {
248 | renderTemplate(renderApp(renderProps, req, res), res)
249 | }
250 | ```
251 |
252 | Absent from the description above is the factory function (the one that takes `renderProps`). It is not strictly necessary in all cases, but it is a convenient way to inject stuff like React-router routes into the response handler scope. The `` component provides the `renderTemplate` and `renderApp` functions. The renderTemplate function does two things: set response status to 200, and sends the response with the stringified template containing your app. A better name for it might be something like `renderResponse`, which it was actually called before I changed it while writing this very chapter. Using your own template rendering function is not currently supported.
253 |
254 | # Future plans
255 |
256 | This is but the very first release of React-response! Plans for the future include:
257 |
258 | - Template engines like Jade and EJS
259 | - Option to use something else than Express
260 | - Add more `Response` components for the following needs:
261 | - Redux
262 | - Redux React Router
263 | - Production stability and more tests
264 | - Integrated reverse proxy for external APIs
265 | - Maybe a Relay server?
266 | - Your suggestions!
267 | - Other cool features
268 |
269 | # Components
270 |
271 | - `ReactServer`
272 | - Props:
273 | - *host*
274 | - Type: string
275 | - Default: '0.0.0.0'
276 | - *port*
277 | - Type: integer
278 | - Default: '3000'
279 | - The wrapper component for React-response. This component instantiates `http` and `Express`.
280 | - `Route`
281 | - Props:
282 | - *path*
283 | - Type: string,
284 | - Default: '/'
285 | - *method*
286 | - Type: string,
287 | - Default: 'get'
288 | - This component establishes a route context for the application handler and middleware. If `get` requests to the `/` route is all you need to serve, you can skip the `Route` component. Everything mounted as children for this component will be served under the specified path.
289 | - `Response`
290 | - Props:
291 | - *template*
292 | - Type: React component
293 | - Default: simple React HTML template
294 | - *handler*
295 | - type: function
296 | - default: `simpleResponse`
297 | - Children:
298 | - *custom render function*
299 | - Type: function
300 | - Arguments: `renderProps`, `req`, `res`
301 | - **OR**
302 | - *your app root element*
303 | - Type: React element
304 | - The Response component creates a route handler for rendering your app. The handler will handle the route specified by the `Route` component, or `get` on `/` by default. Without specifying a custom render function or your app root element as a child to `Response`, React-response will simply render a Hello World page.
305 | - `Middleware`
306 | - Props:
307 | - *use*
308 | - Type function (Express middleware)
309 | - Default: dummy middleware
310 | - Directly applies the specified middleware into Express. Using this generic Middleware component you can accomplish many features that React-response does not currently cater to.
311 | - `createSimpleResponse`
312 | - Arguments:
313 | - Object of props to apply to the component you are rendering
314 | - Simply gives your component, stringified, to the template and sends that as the response.
315 |
316 | By inspecting the source code you might find out that the components can take more props than documented. These exist mainly for testing and decoupling purposes. Usage of undocumented props is not supported, but I am not your mother.
317 |
318 | # Test and build
319 |
320 | I have a very simple React project set up in `/test/consumer` that demonstrates how to use React-response. `cd` into there and run `node ./consumerenv.js` to run the test app. Unit tests are located in `/test`and can be run with `gulp test`. This project uses Tape and Sinon for testing.
321 |
322 | To build the library simply run `gulp build`.
323 |
324 | # Collaboration
325 |
326 | PR's welcome! Please add tests for all changes and follow the general coding style. Semicolons are banned ;)
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | // This gulpfile makes use of new JavaScript features.
4 | // Babel handles this without us having to do anything. It just works.
5 | // You can read more about the new JavaScript features here:
6 | // https://babeljs.io/docs/learn-es2015/
7 |
8 | import gulp from 'gulp'
9 | import gulpLoadPlugins from 'gulp-load-plugins'
10 | import tapSpec from 'tap-spec'
11 |
12 | const $ = gulpLoadPlugins()
13 | const libFolder = 'lib'
14 | const sources = './src/**/*.js'
15 |
16 | gulp.task('default', ['build'])
17 |
18 | gulp.task('watch', ['test'], () => {
19 | gulp.watch([sources, './test/*.js'], ['test'])
20 | })
21 |
22 | // Build as a Node library
23 | gulp.task('build', ['lint', 'test'], () =>
24 | gulp.src(sources)
25 | .pipe($.babel())
26 | // Output files
27 | .pipe(gulp.dest(libFolder))
28 | )
29 |
30 | gulp.task('test', () => {
31 | return gulp.src('./test/*.js')
32 | .pipe($.tape({
33 | reporter: tapSpec()
34 | }).on('error', console.error.bind(console)))
35 | })
36 |
37 | // Lint javascript
38 | gulp.task('lint', () =>
39 | gulp.src(sources)
40 | .pipe($.eslint())
41 | .pipe($.eslint.format())
42 | .pipe($.eslint.failOnError())
43 | )
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-response",
3 | "version": "0.1.1",
4 | "description": "React server-side rendering implementation with minimal configuration.",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "prepublish": "npm run build",
8 | "build": "gulp build",
9 | "test-watch": "gulp watch",
10 | "test": "gulp test"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/danieldunderfelt/react-response.git"
15 | },
16 | "keywords": [
17 | "react",
18 | "server-side rendering",
19 | "server-side",
20 | "ssr",
21 | "react response",
22 | "express",
23 | "universal",
24 | "rendering",
25 | "isomorphic"
26 | ],
27 | "author": {
28 | "name": "Daniel Dunderfelt",
29 | "url": "https://github.com/danieldunderfelt",
30 | "email": "dunderfeltdaniel@gmail.com"
31 | },
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/danieldunderfelt/react-response/issues"
35 | },
36 | "files": [
37 | "LICENSE",
38 | "README.md",
39 | "index.js",
40 | "lib",
41 | "dist"
42 | ],
43 | "homepage": "https://github.com/danieldunderfelt/react-response",
44 | "devDependencies": {
45 | "babel": "^6.3.26",
46 | "babel-core": "^6.4.0",
47 | "babel-eslint": "^5.0.0-beta6",
48 | "babel-plugin-add-module-exports": "^0.1.2",
49 | "babel-preset-es2015": "^6.3.13",
50 | "babel-preset-react": "^6.3.13",
51 | "babel-preset-stage-0": "^6.3.13",
52 | "babel-root-import": "^3.1.0",
53 | "bunyan": "^1.5.1",
54 | "eslint": "^1.10.3",
55 | "eslint-config-airbnb": "^4.0.0",
56 | "eslint-plugin-react": "^3.16.1",
57 | "eslint-plugin-standard": "^1.3.1",
58 | "gulp": "^3.9.0",
59 | "gulp-babel": "^6.1.1",
60 | "gulp-env": "^0.2.0",
61 | "gulp-eslint": "^1.1.1",
62 | "gulp-load-plugins": "^1.2.0",
63 | "gulp-tape": "0.0.7",
64 | "gulp-util": "^3.0.6",
65 | "node-monkey": "^0.2.16",
66 | "piping": "^0.3.0",
67 | "sinon": "^1.17.3",
68 | "tap-spec": "^4.1.1",
69 | "tape": "^4.4.0"
70 | },
71 | "library": {
72 | "name": "react-response",
73 | "entry": "index.js"
74 | },
75 | "peerDependencies": {
76 | "express": "^4.13.4",
77 | "react": "^0.14.0",
78 | "react-dom": "^0.14.0"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/ReactServer.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import Express from 'express'
3 | import http from 'http'
4 | import https from 'https'
5 |
6 | const factory = () => {
7 |
8 | const buildServer = (props) => {
9 | const { server, serverApp, host, port, protocol, children } = props
10 |
11 | const serverInstance = new (server[protocol])(serverApp)
12 |
13 | const config = {
14 | host,
15 | port,
16 | protocol
17 | }
18 |
19 | return {
20 | server: serverInstance,
21 | serverApp,
22 | config,
23 | children
24 | }
25 | }
26 |
27 | class ReactServer extends Component {
28 |
29 | static propTypes = {
30 | host: PropTypes.string.isRequired,
31 | port: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
32 | serverApp: PropTypes.func.isRequired,
33 | server: PropTypes.object.isRequired
34 | };
35 |
36 | static defaultProps = {
37 | host: 'localhost',
38 | port: 3000,
39 | protocol: 'http',
40 | server: { http: http.Server, https: https.Server },
41 | serverApp: new Express()
42 | };
43 |
44 | static buildServer = buildServer;
45 | }
46 |
47 | return ReactServer
48 | }
49 |
50 | export default factory()
--------------------------------------------------------------------------------
/src/Response.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 | import ReactDOM from 'react-dom/server'
3 | import { createSimpleResponse } from './handlers/simpleResponse'
4 | import ReactResponseGreeter from './utils/ReactResponseGreeter'
5 | import Html from './utils/Html'
6 | import { createTemplateString } from './utils/createTemplateString'
7 |
8 | const factory = () => {
9 |
10 | const defaultRenderingFunction = ReactElement => (renderProps, req, res) => ({
11 | component: ReactDOM.renderToString(
12 | React.cloneElement(ReactElement, { ...renderProps })
13 | )
14 | })
15 |
16 | const createTemplatePropsProvider = (children, renderFunction) => {
17 |
18 | if(React.isValidElement(children)) {
19 | // If it is the thing we want to render
20 | return renderFunction(children)
21 | }
22 |
23 | // Otherwise we take it in good faith that it is a custom render function.
24 | return children
25 | }
26 |
27 | const createRenderResponse = Template => (templateProps, res) => {
28 | res.status(200).send(createTemplateString(templateProps, Template))
29 | }
30 |
31 | const buildServer = (props, parent) => {
32 | const { handler, template, children, renderFunction } = props
33 |
34 | const responseHandler = handler(
35 | createRenderResponse(template),
36 | createTemplatePropsProvider(children, renderFunction)
37 | )
38 |
39 | const route = typeof parent.route === "undefined" ?
40 | props :
41 | parent.route
42 |
43 | parent.serverApp[route.method](route.path, responseHandler)
44 |
45 | return {
46 | response: {
47 | handler: responseHandler
48 | }
49 | }
50 | }
51 |
52 | class Response extends Component {
53 |
54 | static propTypes = {
55 | template: PropTypes.func.isRequired,
56 | path: PropTypes.string.isRequired,
57 | method: PropTypes.string.isRequired,
58 | children: PropTypes.oneOfType([PropTypes.func, PropTypes.element]).isRequired,
59 | handler: PropTypes.func.isRequired,
60 | renderFunction: PropTypes.func.isRequired
61 | };
62 |
63 | static defaultProps = {
64 | path: "/",
65 | method: "get",
66 | template: Html,
67 | children: ,
68 | handler: createSimpleResponse(),
69 | renderFunction: defaultRenderingFunction
70 | };
71 |
72 | static buildServer = buildServer;
73 | }
74 |
75 | return Response
76 | }
77 |
78 | export default factory()
--------------------------------------------------------------------------------
/src/Route.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes, Component } from 'react'
2 |
3 | const factory = () => {
4 |
5 | const buildServer = (props) => ({
6 | route: {
7 | path: props.path,
8 | method: props.method
9 | },
10 | children: props.children
11 | })
12 |
13 | class Route extends Component {
14 |
15 | static propTypes = {
16 | path: PropTypes.string.isRequired,
17 | method: PropTypes.string.isRequired
18 | };
19 |
20 | static defaultProps = {
21 | path: "/",
22 | method: "get"
23 | };
24 |
25 | static buildServer = buildServer;
26 | }
27 |
28 | return Route
29 | }
30 |
31 | export default factory()
--------------------------------------------------------------------------------
/src/handlers/simpleResponse.js:
--------------------------------------------------------------------------------
1 | export const createSimpleResponse = (renderProps = {}) => (renderTemplate, renderApp) => (req, res) => {
2 | renderTemplate(renderApp(renderProps, req, res), res)
3 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import ReactServer from './ReactServer'
2 | import Response from './Response'
3 | import Route from './Route'
4 | import { createServer, serve } from './server'
5 | import { Middleware, Favicon, Static } from './middleware'
6 | import { createSimpleResponse } from './handlers/simpleResponse'
7 |
8 | export default {
9 | ReactServer,
10 | Response,
11 | Route,
12 | createServer,
13 | serve,
14 | Middleware,
15 | Favicon,
16 | Static,
17 | createSimpleResponse
18 | }
--------------------------------------------------------------------------------
/src/middleware/Favicon.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import Middleware from './Middleware'
3 | import serveFavicon from 'serve-favicon'
4 | import path from 'path'
5 |
6 | class Favicon extends React.Component {
7 |
8 | static propTypes = {
9 | path: PropTypes.string.isRequired,
10 | faviconMiddleware: PropTypes.func.isRequired
11 | };
12 |
13 | static defaultProps = {
14 | faviconMiddleware: serveFavicon
15 | };
16 |
17 | static buildServer(props, parent) {
18 | return Middleware.buildServer({ use: props.faviconMiddleware(props.path) }, parent)
19 | }
20 | }
21 |
22 | export default Favicon
--------------------------------------------------------------------------------
/src/middleware/Middleware.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 |
3 | class Middleware extends React.Component {
4 |
5 | static propTypes = {
6 | use: PropTypes.func.isRequired
7 | };
8 |
9 | static defaultProps = {
10 | use: (req, res) => console.warning("Empty middleware added.")
11 | };
12 |
13 | static buildServer(props, parent) {
14 |
15 | parent.serverApp.use(
16 | parent.route.path,
17 | props.use
18 | )
19 |
20 | return {
21 | middleware: {
22 | route: parent.route.path,
23 | use: props.use
24 | }
25 | }
26 | }
27 | }
28 |
29 | export default Middleware
--------------------------------------------------------------------------------
/src/middleware/Static.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react'
2 | import Middleware from './Middleware'
3 | import path from 'path'
4 | import Express from 'express'
5 |
6 | class Static extends React.Component {
7 |
8 | static propTypes = {
9 | path: PropTypes.string.isRequired,
10 | staticMiddleware: PropTypes.func.isRequired
11 | };
12 |
13 | static defaultProps = {
14 | staticMiddleware: Express.static
15 | };
16 |
17 | static buildServer(props, parent) {
18 | return Middleware.buildServer({ use: props.staticMiddleware(props.path) }, parent)
19 | }
20 | }
21 |
22 | export default Static
--------------------------------------------------------------------------------
/src/middleware/index.js:
--------------------------------------------------------------------------------
1 | import Middleware from './Middleware'
2 | import Favicon from './Favicon'
3 | import Static from './Static'
4 |
5 | export default {
6 | Middleware,
7 | Favicon,
8 | Static
9 | }
--------------------------------------------------------------------------------
/src/server/createServer.js:
--------------------------------------------------------------------------------
1 | export const createServer = (serverElement, parent = {}) => {
2 |
3 | if(typeof serverElement.type.buildServer === 'function') {
4 | const serverComponents = serverElement.type.buildServer(serverElement.props, parent)
5 |
6 | const nextParent = Object.assign(
7 | {},
8 | parent,
9 | serverComponents,
10 | { children: false } // It is not necessary to carry child React Elements here.
11 | )
12 |
13 | if(typeof serverComponents.children !== "undefined") {
14 |
15 | let children = serverComponents.children
16 |
17 | if(serverComponents.children instanceof Array === false) {
18 | children = [serverComponents.children]
19 | }
20 |
21 | serverComponents.children = children.map(el => createServer(el, nextParent))
22 | }
23 |
24 | return serverComponents
25 | }
26 |
27 | return false
28 | }
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | import { createServer } from './createServer'
2 | import { serve } from './serve'
3 |
4 | export {
5 | createServer,
6 | serve
7 | }
--------------------------------------------------------------------------------
/src/server/runServer.js:
--------------------------------------------------------------------------------
1 | export const runServer = (server, config) => {
2 |
3 | if(config.port) {
4 | server.listen(config.port, config.host, (err) => {
5 | if(err) {
6 | console.error(err)
7 | }
8 | console.info(`==> 💻 Open ${config.protocol}://${config.host}:${config.port} in a browser to view the app.`)
9 | })
10 | } else {
11 | console.error('==> ERROR: No PORT variable has been specified')
12 | }
13 | }
--------------------------------------------------------------------------------
/src/server/serve.js:
--------------------------------------------------------------------------------
1 | import { runServer } from './runServer'
2 |
3 | // I admit, this wasn't a very complicated function.
4 | export const serve = (server) => {
5 | runServer(server.server, server.config)
6 | }
--------------------------------------------------------------------------------
/src/utils/Html.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import ReactDOM from 'react-dom/server'
3 |
4 | /**
5 | * Wrapper component containing HTML metadata and boilerplate tags.
6 | * Used in server-side code only to wrap the string output of the
7 | * rendered route component.
8 | *
9 | * The only thing this component doesn't (and can't) include is the
10 | * HTML doctype declaration, which is added to the rendered output
11 | * by the server.js file.
12 | */
13 | class Html extends Component {
14 |
15 | static propTypes = {
16 | component: PropTypes.string
17 | };
18 |
19 | render() {
20 | const { component } = this.props
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 | }
34 |
35 | export default Html
--------------------------------------------------------------------------------
/src/utils/ReactResponseGreeter.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | class ReactResponseGreeter extends React.Component {
4 |
5 | render() {
6 |
7 | return (
8 |
13 |
18 | Hello world from React Response!
19 |
20 |
21 | You're almost there! To render your own app instead of this
22 | Hello World page, pass the root component of your app as a
23 | child to the Response component.
24 |
25 |
26 | )
27 | }
28 | }
29 |
30 | export default ReactResponseGreeter
--------------------------------------------------------------------------------
/src/utils/createTemplateString.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/server'
3 |
4 | export const createTemplateString = (props, Template) => `
5 |
6 | ${ ReactDOM.renderToStaticMarkup() }
7 | `
--------------------------------------------------------------------------------
/test/Middleware.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import React from 'react'
3 | import { Favicon, Static, Middleware } from '../src/middleware'
4 | import sinon from 'sinon'
5 |
6 | test('Middleware has a buildServer method', t => {
7 | const el =
8 |
9 | t.equal(typeof el.type.buildServer, 'function', 'Middleware.buildServer is a function.')
10 | t.end()
11 | })
12 |
13 | test('buildServer applies middleware on the server app.', t => {
14 | const serverApp = sinon.spy({ use(path, fn) {} }, 'use')
15 | const middlewareStub = sinon.stub()
16 |
17 | const parent = {
18 | serverApp: {
19 | use: serverApp
20 | },
21 | route: {
22 | path: '/'
23 | }
24 | }
25 |
26 | const el =
27 |
28 | const result = Middleware.buildServer(el.props, parent)
29 |
30 | t.ok(serverApp.calledWithExactly('/', middlewareStub), 'Middleware is applied to server at the specified path.')
31 | t.deepEqual(result, {
32 | middleware: {
33 | route: '/',
34 | use: middlewareStub
35 | }
36 | }, 'Middleware compiles props and parent props correctly.')
37 |
38 | t.end()
39 | })
40 |
41 | test('Favicon works like generic Middleware component', t => {
42 | const faviconPath = './consumer/helpers/favicon.ico'
43 |
44 | const serverApp = sinon.spy({ use(path, fn) {} }, 'use')
45 | const faviconStub = sinon.stub()
46 | faviconStub.returns('favicon!')
47 |
48 | const el =
49 |
50 | const parent = {
51 | serverApp: {
52 | use: serverApp
53 | },
54 | route: {
55 | path: '/trolololol'
56 | }
57 | }
58 |
59 | const result = Favicon.buildServer(el.props, parent)
60 |
61 | t.ok(faviconStub.calledWithExactly(faviconPath))
62 | t.ok(serverApp.calledWithExactly('/trolololol', 'favicon!'), 'Middleware is applied to server at the specified path.')
63 | t.deepEqual(result, {
64 | middleware: {
65 | route: '/trolololol',
66 | use: 'favicon!'
67 | }
68 | }, 'Middleware compiles props and parent props correctly.')
69 |
70 | t.end()
71 | })
72 |
73 | test('Static works like generic Middleware component', t => {
74 | const staticPath = './consumer/helpers'
75 |
76 | const serverApp = sinon.spy({ use(path, fn) {} }, 'use')
77 | const staticStub = sinon.stub()
78 | staticStub.returns('static!')
79 |
80 | const el =
81 |
82 | const parent = {
83 | serverApp: {
84 | use: serverApp
85 | },
86 | route: {
87 | path: '/trolololol'
88 | }
89 | }
90 |
91 | const result = Static.buildServer(el.props, parent)
92 |
93 | t.ok(staticStub.calledWithExactly(staticPath))
94 | t.ok(serverApp.calledWithExactly('/trolololol', 'static!'), 'Middleware is applied to server at the specified path.')
95 | t.deepEqual(result, {
96 | middleware: {
97 | route: '/trolololol',
98 | use: 'static!'
99 | }
100 | }, 'Middleware compiles props and parent props correctly.')
101 |
102 | t.end()
103 | })
--------------------------------------------------------------------------------
/test/ReactServer.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import React from 'react'
3 | import ReactServer from '../src/ReactServer'
4 | import Null from './helpers/Null'
5 | import sinon from 'sinon'
6 | import http from 'http'
7 | import Express from 'express'
8 |
9 | test('buildServer exists', t => {
10 | const el =
11 |
12 | t.equal(typeof el.type.buildServer, 'function', 'ReactServer.buildServer is a function.')
13 | t.end()
14 | })
15 |
16 | test('buildServer returns formatted server config of ReactServers props', t => {
17 |
18 | const config = {
19 | host: 'localhost',
20 | port: 4000,
21 | protocol: 'http'
22 | }
23 |
24 | const el = (
25 |
26 |
27 |
28 | )
29 |
30 | const result = ReactServer.buildServer(el.props)
31 |
32 | t.deepEqual(result.config, config, 'The server config is returned.')
33 | t.deepEqual(result.children, , 'The children are included in the result.')
34 |
35 | t.end()
36 | })
37 |
38 | test('buildServer instantiates the passed server', t => {
39 | const props = {
40 | server: http.Server,
41 | serverApp: Express
42 | }
43 |
44 | const serverMock = sinon.spy(props, 'server')
45 | const serverAppMock = sinon.spy(props, 'serverApp')
46 |
47 | const config = {
48 | server: { http: serverMock, https: class {} },
49 | serverApp: serverAppMock
50 | }
51 |
52 | const el = (
53 |
54 |
55 |
56 | )
57 |
58 | const result = ReactServer.buildServer(el.props)
59 |
60 | t.ok(serverMock.calledWith(serverAppMock), 'The server was instantiated.')
61 | t.ok(serverMock.calledOnce, 'The server was called once.')
62 | t.ok(result.server instanceof serverMock, 'The server instance is returned')
63 | t.deepEqual(result.serverApp, serverAppMock, 'The server app is returned')
64 |
65 | t.end()
66 | })
--------------------------------------------------------------------------------
/test/Response.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import sinon from 'sinon'
3 | import React from 'react'
4 | import Null from './helpers/Null'
5 | import Response from '../src/Response'
6 |
7 | test('Response has a buildServer method', t => {
8 | const el =
9 |
10 | t.equal(typeof el.type.buildServer, 'function', 'Response.buildServer is a function.')
11 | t.end()
12 | })
13 |
14 | test('Response runs the passed handler', t => {
15 | const handler = sinon.spy()
16 |
17 | const parent = {
18 | serverApp: {
19 | get(path, routeHandler) {}
20 | }
21 | }
22 | const renderFn = sinon.stub()
23 |
24 | const el = (
25 |
29 | )
30 |
31 | Response.buildServer(el.props, parent)
32 |
33 | t.ok(handler.calledOnce, 'The handler was called once.')
34 | t.ok(handler.calledWith(sinon.match.func, renderFn),
35 | 'The handler was called with the correct arguments.')
36 |
37 | t.end()
38 | })
39 |
40 | test('Response applies the app handler to the Express route', t => {
41 | const handler = sinon.stub()
42 | const responseHandler = sinon.stub()
43 |
44 | handler.returns(responseHandler)
45 |
46 | const parent = {
47 | serverApp: {
48 | get(path, handler) {}
49 | }
50 | }
51 |
52 | const serverSpy = sinon.spy(parent.serverApp, 'get')
53 |
54 | const el = (
55 |
58 | )
59 |
60 | Response.buildServer(el.props, parent)
61 |
62 | t.ok(serverSpy.calledOnce, 'The serverApp route was called once.')
63 | t.ok(serverSpy.calledWithExactly('/', responseHandler),
64 | 'The responseHandler was attached to the route.')
65 |
66 | t.end()
67 | })
68 |
69 | test('Response creates a rendering function for child React elements', t => {
70 | const handler = sinon.stub()
71 | const renderFunction = sinon.stub()
72 | renderFunction.returns('app rendered to string')
73 |
74 | const template = sinon.stub()
75 | const childComponent =
76 |
77 | const parent = {
78 | serverApp: {
79 | get(path, handler) {}
80 | }
81 | }
82 |
83 | const el = (
84 |
85 | { childComponent }
86 |
87 | )
88 |
89 | Response.buildServer(el.props, parent)
90 |
91 | t.ok(handler.calledWith(sinon.match.func, 'app rendered to string'), 'handler called with rendering function for passed element.')
92 | t.ok(renderFunction.calledWith(childComponent), 'The render function was called with the passed child component.')
93 |
94 | t.end()
95 | })
96 |
97 | test('Response returns the rendering function if passed', t => {
98 | const handler = sinon.stub()
99 | const template = sinon.stub()
100 | const renderingFn = sinon.stub()
101 |
102 | const parent = {
103 | serverApp: {
104 | get(path, handler) {}
105 | }
106 | }
107 |
108 | const el = (
109 |
110 | { renderingFn }
111 |
112 | )
113 |
114 | Response.buildServer(el.props, parent)
115 |
116 | t.ok(handler.calledWith(sinon.match.func, renderingFn), 'handler called with custom rendering function.')
117 |
118 | t.end()
119 | })
--------------------------------------------------------------------------------
/test/consumer/consumerenv.js:
--------------------------------------------------------------------------------
1 | require('../helpers/testenv')
2 |
3 | if (!require('piping')({
4 | hook: true,
5 | ignore: /(\/\.|~$|\.json|\.scss$)/i
6 | })) {
7 | return
8 | }
9 |
10 | /*var nomo = require('node-monkey').start(),
11 | bunyan = require('bunyan')
12 |
13 | var log = bunyan.createLogger({
14 | name: 'app',
15 | streams: [
16 | {
17 | level: 'info',
18 | stream: nomo.stream
19 | }
20 | ]
21 | })*/
22 |
23 | require('./test')
--------------------------------------------------------------------------------
/test/consumer/helpers/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danieldunderfelt/react-response/67660d896815c9cd8186da251e14773103567473/test/consumer/helpers/favicon.ico
--------------------------------------------------------------------------------
/test/consumer/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Route, IndexRoute } from 'react-router'
3 |
4 | function App(props) { return App { props.children }
}
5 | export function Home() { return Home
}
6 |
7 | export default (
8 |
9 |
10 |
11 | )
--------------------------------------------------------------------------------
/test/consumer/test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/server'
3 | import path from 'path'
4 | import routes, { Home } from './routes'
5 | import compression from 'compression'
6 | import { RouterContext } from 'react-router'
7 |
8 |
9 | class Test extends React.Component {
10 |
11 | render() {
12 |
13 | return (
14 |
15 |
16 | Trolololo!
17 |
18 |
19 | )
20 | }
21 | }
22 |
23 |
24 | import Html from './../../src/utils/Html'
25 |
26 | import ReactResponse from '../../src'
27 | const {
28 | ReactServer,
29 | Route,
30 | Response,
31 | serve,
32 | createServer,
33 | Middleware,
34 | Static,
35 | Favicon
36 | } = ReactResponse
37 |
38 | const server = createServer(
39 |
40 |
41 |
42 |
43 |
44 | )
45 |
46 | serve(server)
--------------------------------------------------------------------------------
/test/createServer.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import sinon from 'sinon'
3 | import { createServer } from '../src/server'
4 |
5 | test('createServer returns buildServer result.', t => {
6 | const okServerElement = {
7 | type: {
8 | buildServer() { return 'trololol' }
9 | }
10 | }
11 |
12 | t.equal(createServer(okServerElement), 'trololol', 'buildServer result is returned.')
13 |
14 | const notOkServerElement = {
15 | type: {
16 | buildServer: 'trololol'
17 | }
18 | }
19 |
20 | t.notOk(createServer(notOkServerElement), 'createServer returns false if buildServer is not a function.')
21 |
22 | t.end()
23 | })
24 |
25 | test('createServer recurses over children.', t => {
26 | let levels = 0
27 |
28 | const childSpy = sinon.spy()
29 |
30 | const serverElement = {
31 | type: {
32 | buildServer(props, parent) {
33 | levels++
34 |
35 | childSpy()
36 |
37 | return levels > 2 ? {} : {
38 | children: Object.assign({}, serverElement)
39 | }
40 | }
41 | }
42 | }
43 |
44 | createServer(serverElement)
45 |
46 | t.ok(childSpy.calledThrice, 'buildServer calls made for each child.')
47 |
48 | t.end()
49 | })
50 |
51 | test('parent props are passed down to children.', t => {
52 | let levels = 0
53 |
54 | const serverElement = {
55 | type: {
56 | buildServer(props, parent) {
57 | levels++
58 |
59 | return levels > 2 ? { counter: parent.counter + 1 } : {
60 | counter: parent.counter + 1,
61 | children: Object.assign({}, serverElement)
62 | }
63 | }
64 | }
65 | }
66 |
67 | const result = createServer(serverElement, { counter: 0 })
68 |
69 | t.equals(
70 | result.children[0].children[0].counter,
71 | 3,
72 | 'Count increased for each recursion'
73 | )
74 |
75 | t.end()
76 | })
--------------------------------------------------------------------------------
/test/helpers/Null.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | class Null extends React.Component {
4 |
5 | render() {
6 |
7 | return (
8 |
9 | NULL
10 |
11 | )
12 | }
13 | }
14 |
15 | export default Null
--------------------------------------------------------------------------------
/test/helpers/testenv.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var path = require('path')
3 |
4 | var babelrc = fs.readFileSync(path.resolve(__dirname, '../../.babelrc'))
5 | var config
6 |
7 | try {
8 | config = JSON.parse(babelrc)
9 | } catch (err) {
10 | console.error('==> ERROR: Error parsing your .babelrc.')
11 | console.error(err)
12 | }
13 |
14 | require('babel-core/register')(config)
--------------------------------------------------------------------------------
/test/runServer.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape'
2 | import { runServer } from '../src/server/runServer'
3 | import sinon from 'sinon'
4 |
5 | test('runServer runs the server.', t => {
6 | const serverStub = { listen() {} }
7 | const server = sinon.spy(serverStub, 'listen')
8 |
9 | const config = {
10 | port: 3000,
11 | host: 'localhost'
12 | }
13 |
14 | runServer(serverStub, config)
15 |
16 | t.ok(server.calledWith(3000, 'localhost'), 'runServer calls listen on server with the specified config.')
17 |
18 | t.end()
19 | })
--------------------------------------------------------------------------------