├── .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
This is another injection test 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 | })
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 = '
';
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
') > -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 |
--------------------------------------------------------------------------------