├── .gitignore ├── .jshintrc ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── examples └── simple │ ├── client.js │ └── server.js ├── package.json ├── scripts └── register-babel.js └── src ├── Async.js ├── AsyncComponent.js ├── __tests__ ├── Async-browser-test.js ├── injectIntoMarkup-server-test.js └── renderToString-server-test.js ├── index.js ├── index.web.js ├── injectIntoMarkup.js └── renderToString.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": "vars", 4 | "asi": true, 5 | "expr": true, 6 | "globalstrict": true, 7 | "globals": { 8 | "console": false, 9 | "window": false, 10 | "require": false, 11 | "module": false, 12 | "exports": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0 (unreleased) 2 | 3 | - Rewrite React Async to consume observables instead of fetching state 4 | asynchronously. 5 | 6 | ## 2.1.0 7 | 8 | - Support for React 0.13 9 | 10 | ## 2.0.1 11 | 12 | - Bug fixes to make `isAsyncComponent()` not throw on DOM components. 13 | 14 | ## 2.0.0 15 | 16 | - React Async is now compatible with React >= 0.12.0 only. 17 | 18 | - `ReactAsync.renderComponentToStringWithAsyncState` is renamed to 19 | `ReactAsync.renderToStringAsync`. The old name still works but issues a 20 | deprecation warning. 21 | 22 | ## 1.0.2 23 | 24 | - Setting empty async state when getInitialStateAsync returns `false` 25 | 26 | ## 1.0.1 27 | 28 | - Avoid setting an empty state (set `{}` in that case). 29 | 30 | ## 1.0.0 31 | 32 | - Add support for returning promise from getInitialStateAsync. 33 | 34 | ## 0.10.1 35 | 36 | - fix React.renderComponentToString inside Fiber but. 37 | 38 | ## 0.10.0 39 | 40 | - support for React 0.11 41 | 42 | ## 0.9.4 43 | 44 | - expose `` component directly on `ReactAsync`. 45 | 46 | ## 0.9.3 47 | 48 | - remove BaseMixin.componentWillReceiveProps so `asyncState` only takes effect 49 | during first render. 50 | 51 | ## 0.9.2 52 | 53 | - escape unicode character in JSON data transfered from server to browser 54 | 55 | ## 0.9.1 56 | 57 | - proper escaping for JSON data transfered from server to browser 58 | 59 | ## 0.9.0 60 | 61 | - Preloaded component to defer rendering of async components unless their 62 | state is available. 63 | 64 | ## 0.8.0 65 | 66 | - Hooks to serialize/deserialize async state — stateFromJSON/stateToJSON. 67 | 68 | ## 0.7.0 69 | 70 | - Bump react dep to 0.10.0. 71 | 72 | ## 0.6.1 73 | 74 | - Fix bug with updating injected state (via asyncState prop). 75 | 76 | ## 0.6.0 77 | 78 | - Fibers are now optional, they are only needed if you want to pre-render 79 | React components by fetching async state recursively, e.g. using 80 | `ReactAsync.renderComponentToStringWithAsyncState` 81 | 82 | - `ReactAsync.createClass` is removed, use `React.createClass` with 83 | `ReactAsync.Mixin` mixin instead. 84 | 85 | - `ReactAsync.renderComponent` is removed, use `React.renderComponent` 86 | instead. 87 | 88 | - `ReactAsync.renderComponentToString` is renamed to 89 | `ReactAsync.renderComponentToStringWithAsyncState` 90 | 91 | - Add `ReactAsync.isAsyncComponent` to test if a component is an async 92 | component. 93 | 94 | - Add `ReactAsync.prefetchAsyncState` to prefetch state of an async component. 95 | 96 | ## 0.5.1 97 | 98 | - Check if async component is still mounted before updating its state from and 99 | async call. 100 | 101 | ## 0.5.0 102 | 103 | - `ReactAsync.renderComponentToString` now can accept callback w/ 3rd argument 104 | `data`. In this case data will not be injected automatically into the 105 | markup. 106 | 107 | - `ReactAsync.injectIntoMarkup(markup, data, scripts)` to inject data into 108 | markup as JSON blob and a list of scripts (URLs) as ') > -1); 24 | }) 25 | 26 | it('injects data and scripts into markup', function() { 27 | let data = {foo: 'bar'}; 28 | let scripts = ['./a.js', './b.js']; 29 | 30 | let markup = 'This is another injection test'; 31 | let injected = ReactAsync.injectIntoMarkup(markup, data, scripts); 32 | 33 | assert.ok(injected.indexOf('') > -1) 34 | assert.ok(injected.indexOf('') > -1) 35 | assert.ok(injected.indexOf('') > -1) 36 | }); 37 | 38 | it('appends data and scipt to markup if it does not contain element', function() { 39 | let data = {foo: 'bar'}; 40 | let markup = '
hello
'; 41 | 42 | let injected = ReactAsync.injectIntoMarkup(markup, data); 43 | assert.ok(injected.indexOf(markup) > -1); 44 | assert.ok(injected.indexOf('') > -1); 45 | }); 46 | 47 | it('escapes HTML end tags in JSON before injecting into markup', function() { 48 | let data = {foo: ''}; 49 | let markup = 'Escape test'; 50 | let injected = ReactAsync.injectIntoMarkup(markup, data); 51 | assert.ok(injected.indexOf('') > -1); 61 | }) 62 | }); 63 | -------------------------------------------------------------------------------- /src/__tests__/renderToString-server-test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Promise from 'bluebird'; 3 | import Rx from 'rx'; 4 | import React from 'react'; 5 | import Async, {renderToString} from '../'; 6 | 7 | function defineObservableValue(value) { 8 | return { 9 | id: null, 10 | start() { 11 | return Rx.Observable.fromPromise(Promise.delay(0).then(() => value)); 12 | } 13 | }; 14 | } 15 | 16 | describe('ReactAsync.renderToString (server)', function() { 17 | 18 | it('fetches data before rendering a component', function(done) { 19 | 20 | function observe() { 21 | return {message: defineObservableValue('hello')}; 22 | } 23 | 24 | @Async(observe) 25 | class Component extends React.Component { 26 | 27 | render() { 28 | return
{this.props.message}
; 29 | } 30 | } 31 | 32 | renderToString(, function(err, markup, data) { 33 | if (err) { 34 | return done(err); 35 | } 36 | 37 | assert.ok(markup.indexOf('hello') > -1); 38 | assert.equal(Object.keys(data).length, 1) 39 | let id = Object.keys(data)[0]; 40 | assert.ok(data[id]); 41 | assert.deepEqual(data[id], {message: {id: null, data: 'hello', completed: true}}); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('fetches data before rendering a component with observe defined inline', function(done) { 47 | 48 | @Async 49 | class Component extends React.Component { 50 | 51 | static observe() { 52 | return {message: defineObservableValue('hello')}; 53 | } 54 | 55 | render() { 56 | return
{this.props.message}
; 57 | } 58 | } 59 | 60 | renderToString(, function(err, markup, data) { 61 | if (err) { 62 | return done(err); 63 | } 64 | 65 | assert.ok(markup.indexOf('hello') > -1); 66 | assert.equal(Object.keys(data).length, 1) 67 | let id = Object.keys(data)[0]; 68 | assert.ok(data[id]); 69 | assert.deepEqual(data[id], {message: {id: null, data: 'hello', completed: true}}); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('fetches data before rendering a component defined with React.createClass', function(done) { 75 | 76 | function observe() { 77 | return {message: defineObservableValue('hello, legacy')}; 78 | } 79 | 80 | let LegacyComponent = React.createClass({ 81 | render() { 82 | return
{this.props.message}
; 83 | } 84 | }); 85 | 86 | LegacyComponent = Async(LegacyComponent, observe); 87 | 88 | renderToString(, function(err, markup, data) { 89 | if (err) { 90 | return done(err); 91 | } 92 | 93 | assert.ok(markup.indexOf('hello') > -1); 94 | assert.equal(Object.keys(data).length, 1) 95 | let id = Object.keys(data)[0]; 96 | assert.ok(data[id]); 97 | assert.deepEqual(data[id], {message: {id: null, data: 'hello, legacy', completed: true}}); 98 | done(); 99 | }); 100 | }); 101 | 102 | it('fetches data before rendering a component deep nested', function(done) { 103 | 104 | @Async 105 | class Component extends React.Component { 106 | static observe() { 107 | return {message: defineObservableValue('hello')}; 108 | } 109 | 110 | render() { 111 | return
{this.props.message}
; 112 | } 113 | } 114 | 115 | class Outer extends React.Component { 116 | 117 | render() { 118 | return ; 119 | } 120 | } 121 | 122 | renderToString(, function(err, markup, data) { 123 | if (err) { 124 | return done(err); 125 | } 126 | 127 | assert.ok(markup.indexOf('hello') > -1); 128 | 129 | assert.equal(Object.keys(data).length, 1) 130 | let id = Object.keys(data)[0]; 131 | assert.ok(data[id]); 132 | assert.deepEqual(data[id], {message: {id: null, data: 'hello', completed: true}}); 133 | 134 | done(); 135 | }); 136 | }); 137 | 138 | it('handles async components which have same root node id', function(done) { 139 | 140 | @Async 141 | class Component extends React.Component { 142 | 143 | static observe() { 144 | return {message: defineObservableValue('hello')}; 145 | } 146 | 147 | render() { 148 | return
{this.props.message}
; 149 | } 150 | } 151 | 152 | @Async 153 | class OuterAsync extends React.Component { 154 | 155 | static observe() { 156 | return {className: defineObservableValue('outer')}; 157 | } 158 | 159 | render() { 160 | return ; 161 | } 162 | } 163 | 164 | renderToString(, function(err, markup, data) { 165 | if (err) { 166 | return done(err); 167 | } 168 | 169 | assert.ok(markup.indexOf('hello') > -1); 170 | assert.ok(markup.indexOf('outer') > -1); 171 | 172 | assert.equal(Object.keys(data).length, 2) 173 | done(); 174 | }); 175 | }); 176 | 177 | it('should automatically inject data when only two callback arguments are provided', function(done) { 178 | 179 | @Async 180 | class Component extends React.Component { 181 | 182 | static observe() { 183 | return {message: defineObservableValue('hello')}; 184 | } 185 | 186 | render() { 187 | return
{this.props.message}
; 188 | } 189 | } 190 | 191 | renderToString(, function(err, markup) { 192 | if (err) { 193 | return done(err); 194 | } 195 | 196 | assert.ok(markup.indexOf('hello') > -1); 197 | assert.ok(markup.indexOf('`; 16 | 17 | if (scripts) { 18 | injected = injected + scripts 19 | .map(script => ``) 20 | .join(''); 21 | } 22 | 23 | if (markup.indexOf('') > -1) { 24 | return markup.replace('', injected + '$&'); 25 | } else { 26 | return markup + injected; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/renderToString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015 Andrey Popp <8mayday@gmail.com> 3 | */ 4 | 5 | import React from 'react'; 6 | import invariant from 'react/lib/invariant'; 7 | import injectIntoMarkup from './injectIntoMarkup'; 8 | 9 | let Fiber; 10 | try { 11 | Fiber = require('fibers'); 12 | } catch(err) { 13 | // do nothing 14 | } 15 | 16 | /** 17 | * An alternative async version of React.renderToString() which 18 | * fetches data for all async components recursively first. 19 | */ 20 | export default function renderToString(element, cb) { 21 | invariant( 22 | Fiber !== undefined, 23 | 'ReactAsync.renderToString(): cannot import "fibers" package, ' + 24 | 'you need to have it installed to use this function. ' + 25 | 'Install it by running the following command "npm install fibers" ' + 26 | 'in the project directory.' 27 | ); 28 | 29 | let fiber = Fiber(function() { 30 | try { 31 | Fiber.current.__reactAsyncDataPacket__ = {}; 32 | 33 | let data = Fiber.current.__reactAsyncDataPacket__; 34 | let markup = React.renderToString(element); 35 | 36 | // Inject data if callback doesn't receive the data argument 37 | if (cb.length === 2) { 38 | markup = injectIntoMarkup(markup, data); 39 | } 40 | 41 | cb(null, markup, data); 42 | } catch(e) { 43 | cb(e); 44 | } finally { 45 | delete Fiber.current.__reactAsyncDataPacket__; 46 | } 47 | }); 48 | 49 | fiber.run(); 50 | } 51 | --------------------------------------------------------------------------------