├── .eslintrc
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── README.md
├── index.es6.js
├── package.json
├── src
├── client.es6.js
└── server.es6.js
└── test
└── index.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 |
4 | "root": true,
5 |
6 | "env": {
7 | // I write for browser
8 | "browser": true,
9 | // in CommonJS
10 | "node": true
11 | },
12 |
13 | "extends": "eslint:recommended",
14 |
15 | // plugins let use use custom rules below
16 | "plugins": [
17 | "babel",
18 | "react"
19 | ],
20 |
21 | "ecmaFeatures": {
22 | "jsx": true,
23 | "es6": true,
24 | },
25 |
26 | "rules": {
27 | // 0 = off
28 | // 1 = warn
29 | // 2 = error
30 |
31 | // React specifc rules
32 | "react/jsx-boolean-value": 0,
33 | "react/jsx-closing-bracket-location": 1,
34 | "react/jsx-curly-spacing": [2, "always"],
35 | "react/jsx-indent-props": [1, 2],
36 | "react/jsx-no-undef": 1,
37 | "react/jsx-uses-react": 1,
38 | "react/jsx-uses-vars": 1,
39 | "react/wrap-multilines": 1,
40 | "react/react-in-jsx-scope": 1,
41 | "react/prefer-es6-class": 1,
42 | // no binding functions in render for perf
43 | "react/jsx-no-bind": 1,
44 |
45 | // handle async/await functions correctly
46 | // "babel/generator-star-spacing": 1,
47 | // handle object spread
48 | "babel/object-shorthand": 1,
49 |
50 | "indent": [2, 2, {"SwitchCase": 1}],
51 | "max-len": [1, 100, 2, {"ignoreComments": true}],
52 | "no-unused-vars": 1,
53 | "no-console": 0,
54 | // semicolons
55 | "semi": [2, "always"],
56 | "brace-style": [2, "1tbs", { "allowSingleLine": true }],
57 | "comma-dangle": [2, "always-multiline"],
58 | "consistent-return": 0,
59 | "no-underscore-dangle": 0,
60 | "quotes": [2, "single"],
61 | "space-after-keywords": [2, "always"],
62 | "space-before-blocks": [2, "always"],
63 | "space-before-function-parentheses": [0, "never"],
64 | "space-in-brackets": [0, "never"],
65 | "space-in-parens": [2, "never"],
66 | "space-return-throw-case": 1,
67 | "space-unary-ops": [1, { "words": true, "nonwords": false }],
68 | "strict": [2, "never"],
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
25 | node_modules
26 |
27 | # Users Environment Variables
28 | .lock-wscript
29 |
30 | start.sh
31 |
32 | workspace.json
33 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "4"
5 | - "5"
6 |
7 | script:
8 | # - npm run lint
9 | - npm run test
10 |
11 | branches:
12 | except:
13 | - staging
14 |
15 | env:
16 | - CXX=g++-4.8
17 |
18 | addons:
19 | apt:
20 | sources:
21 | - ubuntu-toolchain-r-test
22 | packages:
23 | - g++-4.8
24 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to horse-react
2 | ===========================
3 |
4 | So you want to contribute to horse-react? Fantastic! Here's a brief overview on
5 | how best to do so.
6 |
7 | ## What to change
8 |
9 | Here's some examples of things you might want to make a pull request for:
10 |
11 | * New features
12 | * Bugfixes
13 | * Inefficient blocks of code
14 |
15 | If you have a more deeply-rooted problem with how the program is built or some
16 | of the stylistic decisions made in the code, it's best to
17 | [create an issue](https://github.com/reddit/horse-react/issues) before putting
18 | the effort into a pull request. The same goes for new features - it is
19 | best to check the project's direction, existing pull requests, and currently open
20 | and closed issues first.
21 |
22 | ## Style
23 |
24 | * Two spaces, not tabs
25 | * Semicolons are not optional
26 | * All pages should render on the server and the client. The site should be
27 | usable without javascript.
28 | * Review our [style guide](https://github.com/reddit/tree/master/javascript) for
29 | more information.
30 |
31 | Look at existing code to get a good feel for the patterns we use. Please run
32 | tests before submitting any pull requests. Instructions for running tests can
33 | be found in the README.
34 |
35 | ## Using Git appropriately
36 |
37 | 1. [Fork the repository](https://github.com/reddit/horse-react/fork_select) to
38 | your Github account.
39 | 2. Create a *topical branch* - a branch whose name is succint but explains what
40 | you're doing, such as "change-orangered-to-periwinkle"
41 | 3. Make your changes, committing at logical breaks.
42 | 4. Push your branch to your personal account
43 | 5. [Create a pull request](https://help.github.com/articles/using-pull-requests)
44 | 6. Watch for comments or acceptance
45 |
46 | Please make separate branches for unrelated changes!
47 |
48 | ## Licensing
49 |
50 | horse-react is MIT licensed. See details in the LICENSE file. This is a very permissive
51 | scheme, GPL-compatible but without many of the restrictions of GPL.
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | horse-react
2 | ===========
3 |
4 | [](https://travis-ci.org/reddit/horse-react)
5 |
6 | horse-react is an implementation of the
7 | [horse](https://github.com/reddit/horse) application building library, with
8 | some helpers to make working with react easily.
9 |
10 | Go check out that documentation, then return to see how you'd use it with
11 | React a little easier.
12 |
13 | New APIs
14 | --------
15 |
16 | `horse-react` exposes pre-built `render` and `error` functions that you can
17 | hook into, through `ClientApp` and `ServerApp` classes. It expects your
18 | middleware to attach a `layout`, `body`, and `props` property to the `context`
19 | object during the course of your route handling, and at the end, it will render
20 | it out (either with `layout`, if on the server, or it will mount the `body` if
21 | on the client.)
22 |
23 |
24 | A Brief Overview
25 | ----------------
26 |
27 | An example usage might be like: (es6 incoming)
28 |
29 | `routes.es6.js`
30 |
31 | ```javascript
32 | // This is used both client- and server- side, and simply sets up an app with
33 | // routes; in this case, returning React elements.
34 |
35 | import Layout from '../layouts/layout.jsx';
36 | import Index from '../pages/index.jsx';
37 |
38 | function setupRoutes(app) {
39 | app.router.get('/', function *(next) {
40 | this.data = new Map({
41 | user: db.getUser(1)
42 | });
43 |
44 | this.layout = Layout;
45 |
46 | this.body = function(props) {
47 | return ;
48 | });
49 | });
50 | }
51 |
52 | export default setupRoutes;
53 | ```
54 |
55 |
56 | `server.es6.js`
57 |
58 | ```javascript
59 | import koa from 'koa';
60 |
61 | import {ServerReactApp} from 'horse-react';
62 | import setupRoutes from './setupRoutes';
63 |
64 | var server = koa();
65 |
66 | var app = new App();
67 | setupRoutes(app);
68 |
69 | server.use(ServerReactApp.serverRender(app));
70 | ```
71 |
72 | `client.es6.js`
73 |
74 | You'll want to add push state too, but that's outside the scope of our
75 | example.
76 |
77 | ```javascript
78 | import React from 'react';
79 | import {ClientReactApp} from 'horse-react';
80 |
81 | import setupRoutes from './setupRoutes';
82 |
83 | import jQuery as $ from 'jquery';
84 |
85 | var app = new ClientApp({
86 | mountPoint: document.getElementById('app-container')
87 | });
88 |
89 | setupRoutes(app);
90 |
91 | $(function() {
92 | $('body').on('click', 'a', function(e) {
93 | e.preventDefault();
94 | app.render(this.href);
95 | });
96 | });
97 | ```
98 |
99 | Additional Notes
100 | ----------------
101 |
102 | If you want to mount a client application directly on the server-rendered
103 | markup, add `this.staticMarkup` to the context before `serverRender` is called.
104 | Your `layout` should include `!!CONTENT!!` as the magic word where rendered
105 | body markup should be inserted (instead of `{this.children}`.)
106 |
--------------------------------------------------------------------------------
/index.es6.js:
--------------------------------------------------------------------------------
1 | import ClientReactApp from './src/client';
2 | import ServerReactApp from './src/server';
3 |
4 | export {ClientReactApp, ServerReactApp}
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@r/horse-react",
3 | "version": "1.3.0",
4 | "description": "react-specific implementation of `horse`",
5 | "main": "index.es6.js",
6 | "scripts": {
7 | "test": "./node_modules/mocha/bin/mocha test/index.js",
8 | "lint": "eslint . --ext .js,.jsx"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/reddit/horse-react.git"
13 | },
14 | "author": "reddit ",
15 | "license": "MIT",
16 | "bugs": {
17 | "url": "https://github.com/reddit/horse-react/issues"
18 | },
19 | "homepage": "https://github.com/reddit/horse-react",
20 | "dependencies": {
21 | "horse": "^1.3.2",
22 | "react": "^0.14.7",
23 | "react-dom": "^0.14.7"
24 | },
25 | "devDependencies": {
26 | "babel": "^5.0.0",
27 | "babel-plugin-syntax-trailing-function-commas": "^6.5.0",
28 | "babel-plugin-transform-async-to-generator": "^6.7.4",
29 | "babel-plugin-transform-class-properties": "^6.6.0",
30 | "babel-plugin-transform-object-rest-spread": "^6.6.5",
31 | "babel-plugin-transform-react-constant-elements": "^6.5.0",
32 | "babel-plugin-transform-react-inline-elements": "^6.6.5",
33 | "babel-preset-es2015": "^6.6.0",
34 | "babel-preset-react": "^6.5.0",
35 | "chai": "^2.1.0",
36 | "eslint": "^1.10.3",
37 | "mocha": "^2.1.0",
38 | "sinon": "^1.12.2",
39 | "sinon-chai": "^2.7.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/client.es6.js:
--------------------------------------------------------------------------------
1 | import React from 'react-dom';
2 | import { ClientApp } from 'horse';
3 |
4 | class ClientReactApp extends ClientApp {
5 | constructor (props={}) {
6 | super(props);
7 |
8 | if (props.mountPoint) {
9 | this.mountPoint = props.mountPoint;
10 | }
11 |
12 | if (window.bootstrap) {
13 | this.resetState(window.bootstrap);
14 | }
15 |
16 | this.redirect = this.redirect.bind(this);
17 | }
18 |
19 | redirect (status, path) {
20 | if ((typeof status === 'string') && !path) {
21 | path = status;
22 | }
23 |
24 | this.render(path);
25 | }
26 |
27 | buildContext (href) {
28 | const request = this.buildRequest(href);
29 |
30 | // `this` binding, how does it work
31 | return {
32 | ...request,
33 | redirect: this.redirect,
34 | error: this.error,
35 | };
36 | }
37 |
38 | render (href, firstLoad, modifyContext) {
39 | var mountPoint = this.mountPoint;
40 |
41 | if (!mountPoint) {
42 | throw('Please define a `mountPoint` on your ClientApp for the react element to render to.');
43 | }
44 |
45 | var ctx = this.buildContext(href);
46 |
47 | if (modifyContext) {
48 | var ctx = modifyContext(ctx);
49 | }
50 |
51 | if (firstLoad) {
52 | ctx.props = this.getState();
53 | }
54 |
55 | return new Promise((resolve) => {
56 | this.route(ctx).then(() => {
57 | if (ctx.body && typeof ctx.body === 'function') {
58 | try {
59 | this.emitter.once('page:update', resolve);
60 | React.render(ctx.body(ctx.props), mountPoint);
61 | } catch (e) {
62 | this.error(e, ctx, this);
63 | }
64 | }
65 | });
66 | });
67 | }
68 | }
69 |
70 | export default ClientReactApp;
71 |
--------------------------------------------------------------------------------
/src/server.es6.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/server';
3 | import { App } from 'horse';
4 |
5 | class ServerReactApp extends App {
6 | injectBootstrap (format) {
7 | return function * () {
8 | this.props.timings = this.timings;
9 |
10 | var p = Object.assign({}, this.props);
11 |
12 | if (format) {
13 | p = format(p);
14 | }
15 |
16 | delete p.app;
17 | delete p.api;
18 | delete p.manifest;
19 |
20 | const nonce = p.nonce;
21 | delete p.nonce;
22 |
23 | p.data = {};
24 |
25 | var bootstrap = ServerReactApp.safeStringify(p);
26 |
27 | var body = this.body;
28 | var bodyIndex = body.lastIndexOf('