Skip to content

Commit 679402a

Browse files
authored
Improve hydration fixture, support older versions of React (#14118)
* Hydration Fixture: Only load ReactDOMServer if it exists Fixes an issue where the hydration fixture would try to load in ReactDOMServer below version 14. In version 13, string markup methods exist on the React namespace. * DOM Fixtures: Use class component for App.js This was breaking React 0.13.0. * Hydration Fixture: better findDOMNode compatibility This commit fixes an issue where the Hydration DOM fixture was unusable in React 0.13.0 or lower because of newer API usage. It fixes that by avoiding the use of refs to get the textarea reference in the code editor component, using various versions of findDOMNode as required. * Hydration Fixture: Do not show dropdown for single-line errors If an error showed for the hydration fixture, a detail element was used even if no additional lines could display. In that case, this commit changes the component such that it returns a div. * Deeper React version support for hydration fixture This commit adds support for versions 0.4.0 of React and higher for the hydration fixture. The DOM test fixtures themselves do not support down to React 0.4.0, which would be exhaustive. Instead, the Hydration fixture can pick a version to use for its own purposes. By default, this is the version of React used by the fixtures. In the process of doing this, I had to make some updates to the renderer.html document associated with the hydration fixture, and I've added some comments to better document the history of API changes.
1 parent 1204c78 commit 679402a

File tree

9 files changed

+235
-72
lines changed

9 files changed

+235
-72
lines changed

fixtures/dom/public/renderer.js

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,30 @@
1313
var renders = 0;
1414
var failed = false;
1515

16+
var needsReactDOM = getBooleanQueryParam('needsReactDOM');
17+
var needsCreateElement = getBooleanQueryParam('needsCreateElement');
18+
19+
function unmountComponent(node) {
20+
// ReactDOM was moved into a separate package in 0.14
21+
if (needsReactDOM) {
22+
ReactDOM.unmountComponentAtNode(node);
23+
} else if (React.unmountComponentAtNode) {
24+
React.unmountComponentAtNode(node);
25+
} else {
26+
// Unmounting for React 0.4 and lower
27+
React.unmountAndReleaseReactRootNode(node);
28+
}
29+
}
30+
31+
function createElement(value) {
32+
// React.createElement replaced function invocation in 0.12
33+
if (needsCreateElement) {
34+
return React.createElement(value);
35+
} else {
36+
return value();
37+
}
38+
}
39+
1640
function getQueryParam(key) {
1741
var pattern = new RegExp(key + '=([^&]+)(&|$)');
1842
var matches = window.location.search.match(pattern);
@@ -35,20 +59,56 @@
3559
function prerender() {
3660
setStatus('Generating markup');
3761

38-
output.innerHTML = ReactDOMServer.renderToString(
39-
React.createElement(Fixture)
40-
);
62+
return Promise.resolve()
63+
.then(function() {
64+
const element = createElement(Fixture);
65+
66+
// Server rendering moved to a separate package along with ReactDOM
67+
// in 0.14.0
68+
if (needsReactDOM) {
69+
return ReactDOMServer.renderToString(element);
70+
}
71+
72+
// React.renderComponentToString was renamed in 0.12
73+
if (React.renderToString) {
74+
return React.renderToString(element);
75+
}
4176

42-
setStatus('Markup only (No React)');
77+
// React.renderComponentToString became synchronous in React 0.9.0
78+
if (React.renderComponentToString.length === 1) {
79+
return React.renderComponentToString(element);
80+
}
81+
82+
// Finally, React 0.4 and lower emits markup in a callback
83+
return new Promise(function(resolve) {
84+
React.renderComponentToString(element, resolve);
85+
});
86+
})
87+
.then(function(string) {
88+
output.innerHTML = string;
89+
setStatus('Markup only (No React)');
90+
})
91+
.catch(handleError);
4392
}
4493

4594
function render() {
4695
setStatus('Hydrating');
4796

48-
if (ReactDOM.hydrate) {
49-
ReactDOM.hydrate(React.createElement(Fixture), output);
97+
var element = createElement(Fixture);
98+
99+
// ReactDOM was split out into another package in 0.14
100+
if (needsReactDOM) {
101+
// Hydration changed to a separate method in React 16
102+
if (ReactDOM.hydrate) {
103+
ReactDOM.hydrate(element, output);
104+
} else {
105+
ReactDOM.render(element, output);
106+
}
107+
} else if (React.render) {
108+
// React.renderComponent was renamed in 0.12
109+
React.render(element, output);
50110
} else {
51-
ReactDOM.render(React.createElement(Fixture), output);
111+
React.renderComponent(element, output);
52112
}
53113

54114
setStatus(renders > 0 ? 'Re-rendered (' + renders + 'x)' : 'Hydrated');
@@ -85,17 +145,17 @@
85145
setStatus('Failed');
86146
output.innerHTML = 'Please name your root component "Fixture"';
87147
} else {
88-
prerender();
89-
90-
if (getBooleanQueryParam('hydrate')) {
91-
render();
92-
}
148+
prerender().then(function() {
149+
if (getBooleanQueryParam('hydrate')) {
150+
render();
151+
}
152+
});
93153
}
94154
}
95155

96156
function reloadFixture(code) {
97157
renders = 0;
98-
ReactDOM.unmountComponentAtNode(output);
158+
unmountComponent(output);
99159
injectFixture(code);
100160
}
101161

@@ -109,12 +169,12 @@
109169

110170
loadScript(getQueryParam('reactPath'))
111171
.then(function() {
112-
return getBooleanQueryParam('needsReactDOM')
113-
? loadScript(getQueryParam('reactDOMPath'))
114-
: null;
115-
})
116-
.then(function() {
117-
return loadScript(getQueryParam('reactDOMServerPath'));
172+
if (needsReactDOM) {
173+
return Promise.all([
174+
loadScript(getQueryParam('reactDOMPath')),
175+
loadScript(getQueryParam('reactDOMServerPath')),
176+
]);
177+
}
118178
})
119179
.then(function() {
120180
if (failed) {

fixtures/dom/src/components/App.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import '../style.css';
44

55
const React = window.React;
66

7-
function App() {
8-
return (
9-
<div>
10-
<Header />
11-
<Fixtures />
12-
</div>
13-
);
7+
class App extends React.Component {
8+
render() {
9+
return (
10+
<div>
11+
<Header />
12+
<Fixtures />
13+
</div>
14+
);
15+
}
1416
}
1517

1618
export default App;

fixtures/dom/src/components/Header.js

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {parse, stringify} from 'query-string';
2-
import getVersionTags from '../tags';
2+
import VersionPicker from './VersionPicker';
3+
34
const React = window.React;
45

56
class Header extends React.Component {
@@ -9,18 +10,12 @@ class Header extends React.Component {
910
const version = query.version || 'local';
1011
const production = query.production || false;
1112
const versions = [version];
13+
1214
this.state = {version, versions, production};
1315
}
14-
componentWillMount() {
15-
getVersionTags().then(tags => {
16-
let versions = tags.map(tag => tag.name.slice(1));
17-
versions = [`local`, ...versions];
18-
this.setState({versions});
19-
});
20-
}
21-
handleVersionChange(event) {
16+
handleVersionChange(version) {
2217
const query = parse(window.location.search);
23-
query.version = event.target.value;
18+
query.version = version;
2419
if (query.version === 'local') {
2520
delete query.version;
2621
}
@@ -48,7 +43,10 @@ class Header extends React.Component {
4843
width="20"
4944
height="20"
5045
/>
51-
<a href="/">DOM Test Fixtures (v{React.version})</a>
46+
<a href="/">
47+
DOM Test Fixtures (v
48+
{React.version})
49+
</a>
5250
</span>
5351

5452
<div className="header-controls">
@@ -90,17 +88,14 @@ class Header extends React.Component {
9088
<option value="/suspense">Suspense</option>
9189
</select>
9290
</label>
93-
<label htmlFor="react_version">
91+
<label htmlFor="global_version">
9492
<span className="sr-only">Select a version to test</span>
95-
<select
96-
value={this.state.version}
97-
onChange={this.handleVersionChange}>
98-
{this.state.versions.map(version => (
99-
<option key={version} value={version}>
100-
{version}
101-
</option>
102-
))}
103-
</select>
93+
<VersionPicker
94+
id="global_version"
95+
name="global_version"
96+
version={this.state.version}
97+
onChange={this.handleVersionChange}
98+
/>
10499
</label>
105100
</div>
106101
</div>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import getVersionTags from '../tags';
2+
3+
const React = window.React;
4+
5+
class VersionPicker extends React.Component {
6+
constructor(props, context) {
7+
super(props, context);
8+
const version = props.version || 'local';
9+
const versions = [version];
10+
this.state = {versions};
11+
}
12+
13+
componentWillMount() {
14+
getVersionTags().then(tags => {
15+
let versions = tags.map(tag => tag.name.slice(1));
16+
versions = [`local`, ...versions];
17+
this.setState({versions});
18+
});
19+
}
20+
21+
onChange = event => {
22+
this.props.onChange(event.target.value);
23+
};
24+
25+
render() {
26+
const {version, id, name} = this.props;
27+
const {versions} = this.state;
28+
29+
return (
30+
<select id={id} name={name} value={version} onChange={this.onChange}>
31+
{versions.map(version => (
32+
<option key={version} value={version}>
33+
{version}
34+
</option>
35+
))}
36+
</select>
37+
);
38+
}
39+
}
40+
41+
export default VersionPicker;

fixtures/dom/src/components/fixtures/hydration/Code.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {findDOMNode} from '../../../find-dom-node';
2+
13
const React = window.React;
24

35
export class CodeEditor extends React.Component {
@@ -6,6 +8,8 @@ export class CodeEditor extends React.Component {
68
}
79

810
componentDidMount() {
11+
this.textarea = findDOMNode(this);
12+
913
// Important: CodeMirror incorrectly lays out the editor
1014
// if it executes before CSS has loaded
1115
// https://github.com/graphql/graphiql/issues/33#issuecomment-318188555
@@ -44,7 +48,6 @@ export class CodeEditor extends React.Component {
4448
render() {
4549
return (
4650
<textarea
47-
ref={ref => (this.textarea = ref)}
4851
defaultValue={this.props.code}
4952
autoComplete="off"
5053
hidden={true}
@@ -72,6 +75,10 @@ export class CodeError extends React.Component {
7275
if (supportsDetails) {
7376
const [summary, ...body] = error.message.split(/\n+/g);
7477

78+
if (body.length >= 0) {
79+
return <div className={className}>{summary}</div>;
80+
}
81+
7582
return (
7683
<details className={className}>
7784
<summary>{summary}</summary>

fixtures/dom/src/components/fixtures/hydration/hydration.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
.hydration-options label {
2424
font-size: 13px;
25+
margin-right: 10px;
2526
}
2627

2728
.hydration-options input[type=checkbox] {
@@ -30,6 +31,11 @@
3031
vertical-align: middle;
3132
}
3233

34+
.hydration-options select {
35+
margin-left: 10px;
36+
max-width: 100px;
37+
}
38+
3339
.hydration .CodeMirror {
3440
font-size: 13px;
3541
padding-top: 8px;

fixtures/dom/src/components/fixtures/hydration/index.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
import './hydration.css';
2+
import VersionPicker from '../../VersionPicker';
23
import {SAMPLE_CODE} from './data';
34
import {CodeEditor, CodeError} from './Code';
45
import {compile} from './code-transformer';
56
import {reactPaths} from '../../../react-loader';
67
import qs from 'query-string';
78

89
const React = window.React;
10+
// The Hydration fixture can render at a different version than the parent
11+
// app. This allows rendering for versions of React older than the DOM
12+
// test fixtures can support.
13+
const initialVersion = qs.parse(window.location.search).version || 'local';
914

1015
class Hydration extends React.Component {
1116
state = {
1217
error: null,
1318
code: SAMPLE_CODE,
1419
hydrate: true,
20+
version: initialVersion,
1521
};
1622

1723
ready = false;
@@ -72,9 +78,14 @@ class Hydration extends React.Component {
7278
});
7379
};
7480

81+
setVersion = version => {
82+
this.setState({version});
83+
};
84+
7585
render() {
76-
const {code, error, hydrate} = this.state;
77-
const src = '/renderer.html?' + qs.stringify({hydrate, ...reactPaths()});
86+
const {code, error, hydrate, version} = this.state;
87+
const src =
88+
'/renderer.html?' + qs.stringify({hydrate, ...reactPaths(version)});
7889

7990
return (
8091
<div className="hydration">
@@ -89,6 +100,16 @@ class Hydration extends React.Component {
89100
/>
90101
Auto-Hydrate
91102
</label>
103+
104+
<label htmlFor="hydration_version">
105+
Version:
106+
<VersionPicker
107+
id="hydration_version"
108+
name="hyration_version"
109+
version={version}
110+
onChange={this.setVersion}
111+
/>
112+
</label>
92113
</header>
93114

94115
<CodeEditor code={code} onChange={this.setCode} />

0 commit comments

Comments
 (0)