From d39c7434cfa6d887f4d722e8b699268e7c3f5b04 Mon Sep 17 00:00:00 2001 From: Luis Merino Date: Thu, 31 May 2018 11:57:29 +0200 Subject: [PATCH] Migrate codebase to React 16-compatible lifecycles In addition history objects are only created if there's an existing DOM in the runtime. This gives implementors the chance to skip initializing history on runtimes without DOM polyfilling/support, e.g.: a running instance Node.js without js-dom. --- modules/BrowserHistory.js | 41 +++++++++------- modules/HashHistory.js | 25 ++++++---- modules/MemoryHistory.js | 41 +++++++++------- modules/Prompt.js | 33 +++++++++---- modules/__tests__/BrowserHistory-test.js | 5 ++ modules/__tests__/HashHistory-test.js | 5 ++ modules/__tests__/MemoryHistory-test.js | 5 ++ .../RenderTestSequences/PromptUpdates.js | 49 +++++++++++++++++++ .../ReplaceChangesTheKey.js | 1 - .../__tests__/RenderTestSequences/index.js | 1 + package.json | 21 +++++--- 11 files changed, 166 insertions(+), 61 deletions(-) create mode 100644 modules/__tests__/RenderTestSequences/PromptUpdates.js diff --git a/modules/BrowserHistory.js b/modules/BrowserHistory.js index 5af0450..d2eefc1 100644 --- a/modules/BrowserHistory.js +++ b/modules/BrowserHistory.js @@ -1,6 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; import createBrowserHistory from "history/createBrowserHistory"; +import { canUseDOM } from "history/DOMUtils"; import { history as historyType } from "./PropTypes"; /** @@ -24,23 +25,29 @@ class BrowserHistory extends React.Component { return { history: this.history }; } - componentWillMount() { - const { - basename, - forceRefresh, - getUserConfirmation, - keyLength - } = this.props; - - this.history = createBrowserHistory({ - basename, - forceRefresh, - getUserConfirmation, - keyLength - }); - - // Do this here so we catch actions in cDM. - this.unlisten = this.history.listen(() => this.forceUpdate()); + constructor(props) { + super(props); + + if (canUseDOM) { + const { + basename, + forceRefresh, + getUserConfirmation, + keyLength + } = this.props; + + this.history = createBrowserHistory({ + basename, + forceRefresh, + getUserConfirmation, + keyLength + }); + + // Do this here so we catch actions in cDM. + this.unlisten = this.history.listen(() => this.forceUpdate()); + } else { + this.history = {}; + } } componentWillUnmount() { diff --git a/modules/HashHistory.js b/modules/HashHistory.js index 80b882a..9c54d84 100644 --- a/modules/HashHistory.js +++ b/modules/HashHistory.js @@ -1,6 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; import createHashHistory from "history/createHashHistory"; +import { canUseDOM } from "history/DOMUtils"; import { history as historyType } from "./PropTypes"; /** @@ -22,17 +23,23 @@ class HashHistory extends React.Component { return { history: this.history }; } - componentWillMount() { + constructor(props) { + super(props); + const { basename, getUserConfirmation, hashType } = this.props; - this.history = createHashHistory({ - basename, - getUserConfirmation, - hashType - }); - - // Do this here so we catch actions in cDM. - this.unlisten = this.history.listen(() => this.forceUpdate()); + if (canUseDOM) { + this.history = createHashHistory({ + basename, + getUserConfirmation, + hashType + }); + + // Do this here so we catch actions in cDM. + this.unlisten = this.history.listen(() => this.forceUpdate()); + } else { + this.history = {}; + } } componentWillUnmount() { diff --git a/modules/MemoryHistory.js b/modules/MemoryHistory.js index 88e7d64..d8610d6 100644 --- a/modules/MemoryHistory.js +++ b/modules/MemoryHistory.js @@ -1,6 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; import createMemoryHistory from "history/createMemoryHistory"; +import { canUseDOM } from "history/DOMUtils"; import { history as historyType } from "./PropTypes"; /** @@ -23,23 +24,29 @@ class MemoryHistory extends React.Component { return { history: this.history }; } - componentWillMount() { - const { - getUserConfirmation, - initialEntries, - initialIndex, - keyLength - } = this.props; - - this.history = createMemoryHistory({ - getUserConfirmation, - initialEntries, - initialIndex, - keyLength - }); - - // Do this here so we catch actions in cDM. - this.unlisten = this.history.listen(() => this.forceUpdate()); + constructor(props) { + super(props); + + if (canUseDOM) { + const { + getUserConfirmation, + initialEntries, + initialIndex, + keyLength + } = this.props; + + this.history = createMemoryHistory({ + getUserConfirmation, + initialEntries, + initialIndex, + keyLength + }); + + // Do this here so we catch actions in cDM. + this.unlisten = this.history.listen(() => this.forceUpdate()); + } else { + this.history = {}; + } } componentWillUnmount() { diff --git a/modules/Prompt.js b/modules/Prompt.js index 8de3c12..1d41e72 100644 --- a/modules/Prompt.js +++ b/modules/Prompt.js @@ -1,5 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; +import { polyfill } from 'react-lifecycles-compat'; +import { canUseDOM } from "history/DOMUtils"; import { history as historyType } from "./PropTypes"; class Prompt extends React.Component { @@ -16,9 +18,18 @@ class Prompt extends React.Component { when: true }; + constructor(props, context) { + super(props, context); + + if (canUseDOM) { + if (this.props.when) { + this.enable(this.props.message); + } + } + } + enable(message) { if (this.unblock) this.unblock(); - this.unblock = this.context.history.block(message); } @@ -29,17 +40,18 @@ class Prompt extends React.Component { } } - componentWillMount() { - if (this.props.when) this.enable(this.props.message); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.when) { - if (!this.props.when || this.props.message !== nextProps.message) - this.enable(nextProps.message); + getSnapshotBeforeUpdate(prevProps) { + if (this.props.when) { + if (!prevProps.when || prevProps.message !== this.props.message) + this.enable(this.props.message); } else { this.disable(); } + return null; + } + + componentDidUpdate() { + // we must define this lifecycle method as long as we're using the polyfill } componentWillUnmount() { @@ -51,4 +63,7 @@ class Prompt extends React.Component { } } +// Polyfill your component so the new lifecycles will work with older versions of React: +polyfill(Prompt); + export default Prompt; diff --git a/modules/__tests__/BrowserHistory-test.js b/modules/__tests__/BrowserHistory-test.js index 191680f..359c853 100644 --- a/modules/__tests__/BrowserHistory-test.js +++ b/modules/__tests__/BrowserHistory-test.js @@ -114,6 +114,11 @@ describe('BrowserHistory', () => { const children = RenderTestSequences.PromptBlocksTheForwardButton(done) render(, node) }) + + it('updates the message', (done) => { + const children = RenderTestSequences.PromptUpdates(done) + render(, node) + }) }) describe('inactive prompt', () => { diff --git a/modules/__tests__/HashHistory-test.js b/modules/__tests__/HashHistory-test.js index 6b1ecf6..d3078fb 100644 --- a/modules/__tests__/HashHistory-test.js +++ b/modules/__tests__/HashHistory-test.js @@ -104,6 +104,11 @@ describe('HashHistory', () => { const children = RenderTestSequences.PromptBlocksTheForwardButton(done) render(, node) }) + + it('updates the message', (done) => { + const children = RenderTestSequences.PromptUpdates(done) + render(, node) + }) }) describe('"hashbang" hash encoding', () => { diff --git a/modules/__tests__/MemoryHistory-test.js b/modules/__tests__/MemoryHistory-test.js index 1ea3b10..189086f 100644 --- a/modules/__tests__/MemoryHistory-test.js +++ b/modules/__tests__/MemoryHistory-test.js @@ -90,6 +90,11 @@ describe('MemoryHistory', () => { const children = RenderTestSequences.PromptBlocksTheForwardButton(done) render(, node) }) + + it('updates the message', (done) => { + const children = RenderTestSequences.PromptUpdates(done) + render(, node) + }) }) describe('inactive prompt', () => { diff --git a/modules/__tests__/RenderTestSequences/PromptUpdates.js b/modules/__tests__/RenderTestSequences/PromptUpdates.js new file mode 100644 index 0000000..93b1ef6 --- /dev/null +++ b/modules/__tests__/RenderTestSequences/PromptUpdates.js @@ -0,0 +1,49 @@ +import React from 'react' +import expect, { spyOn } from 'expect' +import { Push } from '../../Actions' +import Prompt from '../../Prompt' +import createRenderProp from './createRenderProp' + +export default (done) => { + class TestComponent extends React.Component { + state = { message: 'Are you sure?', when: false } + componentDidMount() { + this.setState({ message: 'Are you totally sure?', when: true }) + } + render() { + return ( +
+ + {this.state.when && } +
+ ) + } + } + + const steps = [ + (history) => { + const { action, location } = history + expect(action).toBe('POP') + expect(location).toMatch({ + pathname: '/' + }) + + spyOn(history, 'block') + + return + }, + ({ block }) => { + expect(block.calls.length).toBe(1) + expect(block).toHaveBeenCalledWith('Are you totally sure?') + expect(location).toMatch({ + pathname: '/hello' + }) + + block.restore() + + return null + } + ] + + return createRenderProp(steps, done) +} diff --git a/modules/__tests__/RenderTestSequences/ReplaceChangesTheKey.js b/modules/__tests__/RenderTestSequences/ReplaceChangesTheKey.js index 73ecb05..22bfd5d 100644 --- a/modules/__tests__/RenderTestSequences/ReplaceChangesTheKey.js +++ b/modules/__tests__/RenderTestSequences/ReplaceChangesTheKey.js @@ -10,7 +10,6 @@ export default (done) => { ({ location }) => { expect(location).toMatch({ pathname: '/', - key: undefined }) return diff --git a/modules/__tests__/RenderTestSequences/index.js b/modules/__tests__/RenderTestSequences/index.js index f4ca854..d8714d3 100644 --- a/modules/__tests__/RenderTestSequences/index.js +++ b/modules/__tests__/RenderTestSequences/index.js @@ -7,6 +7,7 @@ export PromptBlocksAPush from './PromptBlocksAPush' export PromptBlocksAReplace from './PromptBlocksAReplace' export PromptBlocksTheBackButton from './PromptBlocksTheBackButton' export PromptBlocksTheForwardButton from './PromptBlocksTheForwardButton' +export PromptUpdates from './PromptUpdates' export PushNewLocation from './PushNewLocation' export PushAction from './PushAction' export PushWithStateUsesAKey from './PushWithStateUsesAKey' diff --git a/package.json b/package.json index 8ac9f99..fab0436 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ }, "dependencies": { "history": "^4.5.0", - "prop-types": "^15.6.0" + "prop-types": "^15.6.0", + "react-lifecycles-compat": "^3.0.4" }, "peerDependencies": { "react": "15.x" @@ -26,10 +27,10 @@ "babel-cli": "^6.18.0", "babel-eslint": "^7.0.0", "babel-loader": "^6.2.10", - "babel-plugin-transform-react-remove-prop-types": "^0.2.11", - "babel-preset-es2015": "^6.18.0", - "babel-preset-react": "^6.5.0", - "babel-preset-stage-1": "^6.5.0", + "babel-plugin-transform-react-remove-prop-types": "^0.2.12", + "babel-preset-es2015": "^6.24.1", + "babel-preset-react": "^6.24.1", + "babel-preset-stage-1": "^6.24.1", "eslint": "^3.3.1", "eslint-plugin-import": "^2.0.0", "eslint-plugin-react": "^6.1.2", @@ -47,11 +48,15 @@ "karma-webpack": "^1.7.0", "mocha": "^3.0.2", "pretty-bytes": "^4.0.0", - "react": "^15.3.0", - "react-dom": "^15.3.0", + "react": "^16.4.0", + "react-dom": "^16.4.0", "readline-sync": "^1.4.4", "webpack": "1.13.1", "webpack-dev-server": "1.16.2" }, - "keywords": ["react", "history", "link"] + "keywords": [ + "react", + "history", + "link" + ] }