From 679402a66b09e129d06f415cd976d18727cc8590 Mon Sep 17 00:00:00 2001 From: Nathan Hunzaker Date: Wed, 13 Mar 2019 15:12:49 -0700 Subject: [PATCH] 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. --- fixtures/dom/public/renderer.js | 98 +++++++++++++++---- fixtures/dom/src/components/App.js | 16 +-- fixtures/dom/src/components/Header.js | 37 +++---- fixtures/dom/src/components/VersionPicker.js | 41 ++++++++ .../src/components/fixtures/hydration/Code.js | 9 +- .../fixtures/hydration/hydration.css | 6 ++ .../components/fixtures/hydration/index.js | 25 ++++- fixtures/dom/src/find-dom-node.js | 20 ++++ fixtures/dom/src/react-loader.js | 55 ++++++----- 9 files changed, 235 insertions(+), 72 deletions(-) create mode 100644 fixtures/dom/src/components/VersionPicker.js create mode 100644 fixtures/dom/src/find-dom-node.js diff --git a/fixtures/dom/public/renderer.js b/fixtures/dom/public/renderer.js index 542580a073..fa8eca8b63 100644 --- a/fixtures/dom/public/renderer.js +++ b/fixtures/dom/public/renderer.js @@ -13,6 +13,30 @@ var renders = 0; var failed = false; + var needsReactDOM = getBooleanQueryParam('needsReactDOM'); + var needsCreateElement = getBooleanQueryParam('needsCreateElement'); + + function unmountComponent(node) { + // ReactDOM was moved into a separate package in 0.14 + if (needsReactDOM) { + ReactDOM.unmountComponentAtNode(node); + } else if (React.unmountComponentAtNode) { + React.unmountComponentAtNode(node); + } else { + // Unmounting for React 0.4 and lower + React.unmountAndReleaseReactRootNode(node); + } + } + + function createElement(value) { + // React.createElement replaced function invocation in 0.12 + if (needsCreateElement) { + return React.createElement(value); + } else { + return value(); + } + } + function getQueryParam(key) { var pattern = new RegExp(key + '=([^&]+)(&|$)'); var matches = window.location.search.match(pattern); @@ -35,20 +59,56 @@ function prerender() { setStatus('Generating markup'); - output.innerHTML = ReactDOMServer.renderToString( - React.createElement(Fixture) - ); + return Promise.resolve() + .then(function() { + const element = createElement(Fixture); - setStatus('Markup only (No React)'); + // Server rendering moved to a separate package along with ReactDOM + // in 0.14.0 + if (needsReactDOM) { + return ReactDOMServer.renderToString(element); + } + + // React.renderComponentToString was renamed in 0.12 + if (React.renderToString) { + return React.renderToString(element); + } + + // React.renderComponentToString became synchronous in React 0.9.0 + if (React.renderComponentToString.length === 1) { + return React.renderComponentToString(element); + } + + // Finally, React 0.4 and lower emits markup in a callback + return new Promise(function(resolve) { + React.renderComponentToString(element, resolve); + }); + }) + .then(function(string) { + output.innerHTML = string; + setStatus('Markup only (No React)'); + }) + .catch(handleError); } function render() { setStatus('Hydrating'); - if (ReactDOM.hydrate) { - ReactDOM.hydrate(React.createElement(Fixture), output); + var element = createElement(Fixture); + + // ReactDOM was split out into another package in 0.14 + if (needsReactDOM) { + // Hydration changed to a separate method in React 16 + if (ReactDOM.hydrate) { + ReactDOM.hydrate(element, output); + } else { + ReactDOM.render(element, output); + } + } else if (React.render) { + // React.renderComponent was renamed in 0.12 + React.render(element, output); } else { - ReactDOM.render(React.createElement(Fixture), output); + React.renderComponent(element, output); } setStatus(renders > 0 ? 'Re-rendered (' + renders + 'x)' : 'Hydrated'); @@ -85,17 +145,17 @@ setStatus('Failed'); output.innerHTML = 'Please name your root component "Fixture"'; } else { - prerender(); - - if (getBooleanQueryParam('hydrate')) { - render(); - } + prerender().then(function() { + if (getBooleanQueryParam('hydrate')) { + render(); + } + }); } } function reloadFixture(code) { renders = 0; - ReactDOM.unmountComponentAtNode(output); + unmountComponent(output); injectFixture(code); } @@ -109,12 +169,12 @@ loadScript(getQueryParam('reactPath')) .then(function() { - return getBooleanQueryParam('needsReactDOM') - ? loadScript(getQueryParam('reactDOMPath')) - : null; - }) - .then(function() { - return loadScript(getQueryParam('reactDOMServerPath')); + if (needsReactDOM) { + return Promise.all([ + loadScript(getQueryParam('reactDOMPath')), + loadScript(getQueryParam('reactDOMServerPath')), + ]); + } }) .then(function() { if (failed) { diff --git a/fixtures/dom/src/components/App.js b/fixtures/dom/src/components/App.js index 5b0de92e07..e54cc333d8 100644 --- a/fixtures/dom/src/components/App.js +++ b/fixtures/dom/src/components/App.js @@ -4,13 +4,15 @@ import '../style.css'; const React = window.React; -function App() { - return ( -
-
- -
- ); +class App extends React.Component { + render() { + return ( +
+
+ +
+ ); + } } export default App; diff --git a/fixtures/dom/src/components/Header.js b/fixtures/dom/src/components/Header.js index 8fc03246e8..74af700e27 100644 --- a/fixtures/dom/src/components/Header.js +++ b/fixtures/dom/src/components/Header.js @@ -1,5 +1,6 @@ import {parse, stringify} from 'query-string'; -import getVersionTags from '../tags'; +import VersionPicker from './VersionPicker'; + const React = window.React; class Header extends React.Component { @@ -9,18 +10,12 @@ class Header extends React.Component { const version = query.version || 'local'; const production = query.production || false; const versions = [version]; + this.state = {version, versions, production}; } - componentWillMount() { - getVersionTags().then(tags => { - let versions = tags.map(tag => tag.name.slice(1)); - versions = [`local`, ...versions]; - this.setState({versions}); - }); - } - handleVersionChange(event) { + handleVersionChange(version) { const query = parse(window.location.search); - query.version = event.target.value; + query.version = version; if (query.version === 'local') { delete query.version; } @@ -48,7 +43,10 @@ class Header extends React.Component { width="20" height="20" /> - DOM Test Fixtures (v{React.version}) + + DOM Test Fixtures (v + {React.version}) +
@@ -90,17 +88,14 @@ class Header extends React.Component { -
diff --git a/fixtures/dom/src/components/VersionPicker.js b/fixtures/dom/src/components/VersionPicker.js new file mode 100644 index 0000000000..fcccf0947b --- /dev/null +++ b/fixtures/dom/src/components/VersionPicker.js @@ -0,0 +1,41 @@ +import getVersionTags from '../tags'; + +const React = window.React; + +class VersionPicker extends React.Component { + constructor(props, context) { + super(props, context); + const version = props.version || 'local'; + const versions = [version]; + this.state = {versions}; + } + + componentWillMount() { + getVersionTags().then(tags => { + let versions = tags.map(tag => tag.name.slice(1)); + versions = [`local`, ...versions]; + this.setState({versions}); + }); + } + + onChange = event => { + this.props.onChange(event.target.value); + }; + + render() { + const {version, id, name} = this.props; + const {versions} = this.state; + + return ( + + ); + } +} + +export default VersionPicker; diff --git a/fixtures/dom/src/components/fixtures/hydration/Code.js b/fixtures/dom/src/components/fixtures/hydration/Code.js index 26d93b4697..55c44a8c3a 100644 --- a/fixtures/dom/src/components/fixtures/hydration/Code.js +++ b/fixtures/dom/src/components/fixtures/hydration/Code.js @@ -1,3 +1,5 @@ +import {findDOMNode} from '../../../find-dom-node'; + const React = window.React; export class CodeEditor extends React.Component { @@ -6,6 +8,8 @@ export class CodeEditor extends React.Component { } componentDidMount() { + this.textarea = findDOMNode(this); + // Important: CodeMirror incorrectly lays out the editor // if it executes before CSS has loaded // https://github.com/graphql/graphiql/issues/33#issuecomment-318188555 @@ -44,7 +48,6 @@ export class CodeEditor extends React.Component { render() { return (