Skip to content

Commit

Permalink
feat: add nodeRef alternative instead of internal findDOMNode (#559)
Browse files Browse the repository at this point in the history
`ReactDOM.findDOMNode` is deprecated and causes warnings in `StrictMode`, so we're offering users an option to pass the `nodeRef` prop, a ref object which should point to the child node being transitioned.
  • Loading branch information
iamandrewluca committed May 5, 2020
1 parent 13435f8 commit 85016bf
Show file tree
Hide file tree
Showing 19 changed files with 549 additions and 326 deletions.
7 changes: 6 additions & 1 deletion .storybook/config.js
@@ -1,4 +1,9 @@
import { configure } from '@storybook/react';
import { configure, addDecorator } from '@storybook/react';
import React from 'react';

addDecorator(
storyFn => <React.StrictMode>{storyFn()}</React.StrictMode>,
)

function loadStories() {
require('../stories');
Expand Down
47 changes: 35 additions & 12 deletions src/CSSTransition.js
Expand Up @@ -90,61 +90,72 @@ class CSSTransition extends React.Component {
exit: {},
}

onEnter = (node, appearing) => {
onEnter = (maybeNode, maybeAppearing) => {
const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing)
this.removeClasses(node, 'exit');
this.addClass(node, appearing ? 'appear' : 'enter', 'base');

if (this.props.onEnter) {
this.props.onEnter(node, appearing)
this.props.onEnter(maybeNode, maybeAppearing)
}
}

onEntering = (node, appearing) => {
onEntering = (maybeNode, maybeAppearing) => {
const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing)
const type = appearing ? 'appear' : 'enter';
this.addClass(node, type, 'active')

if (this.props.onEntering) {
this.props.onEntering(node, appearing)
this.props.onEntering(maybeNode, maybeAppearing)
}
}

onEntered = (node, appearing) => {
onEntered = (maybeNode, maybeAppearing) => {
const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing)
const type = appearing ? 'appear' : 'enter'
this.removeClasses(node, type);
this.addClass(node, type, 'done');

if (this.props.onEntered) {
this.props.onEntered(node, appearing)
this.props.onEntered(maybeNode, maybeAppearing)
}
}

onExit = (node) => {
onExit = (maybeNode) => {
const [node] = this.resolveArguments(maybeNode)
this.removeClasses(node, 'appear');
this.removeClasses(node, 'enter');
this.addClass(node, 'exit', 'base')

if (this.props.onExit) {
this.props.onExit(node)
this.props.onExit(maybeNode)
}
}

onExiting = (node) => {
onExiting = (maybeNode) => {
const [node] = this.resolveArguments(maybeNode)
this.addClass(node, 'exit', 'active')

if (this.props.onExiting) {
this.props.onExiting(node)
this.props.onExiting(maybeNode)
}
}

onExited = (node) => {
onExited = (maybeNode) => {
const [node] = this.resolveArguments(maybeNode)
this.removeClasses(node, 'exit');
this.addClass(node, 'exit', 'done');

if (this.props.onExited) {
this.props.onExited(node)
this.props.onExited(maybeNode)
}
}

// when prop `nodeRef` is provided `node` is excluded
resolveArguments = (maybeNode, maybeAppearing) => this.props.nodeRef
? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing`
: [maybeNode, maybeAppearing] // `findDOMNode` was used

getClassNames = (type) => {
const { classNames } = this.props;
const isStringClassNames = typeof classNames === 'string';
Expand Down Expand Up @@ -306,6 +317,8 @@ CSSTransition.propTypes = {
* A `<Transition>` callback fired immediately after the 'enter' or 'appear' class is
* applied.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* @type Function(node: HtmlElement, isAppearing: bool)
*/
onEnter: PropTypes.func,
Expand All @@ -314,6 +327,8 @@ CSSTransition.propTypes = {
* A `<Transition>` callback fired immediately after the 'enter-active' or
* 'appear-active' class is applied.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* @type Function(node: HtmlElement, isAppearing: bool)
*/
onEntering: PropTypes.func,
Expand All @@ -322,6 +337,8 @@ CSSTransition.propTypes = {
* A `<Transition>` callback fired immediately after the 'enter' or
* 'appear' classes are **removed** and the `done` class is added to the DOM node.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* @type Function(node: HtmlElement, isAppearing: bool)
*/
onEntered: PropTypes.func,
Expand All @@ -330,13 +347,17 @@ CSSTransition.propTypes = {
* A `<Transition>` callback fired immediately after the 'exit' class is
* applied.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed
*
* @type Function(node: HtmlElement)
*/
onExit: PropTypes.func,

/**
* A `<Transition>` callback fired immediately after the 'exit-active' is applied.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed
*
* @type Function(node: HtmlElement)
*/
onExiting: PropTypes.func,
Expand All @@ -345,6 +366,8 @@ CSSTransition.propTypes = {
* A `<Transition>` callback fired immediately after the 'exit' classes
* are **removed** and the `exit-done` class is added to the DOM node.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed
*
* @type Function(node: HtmlElement)
*/
onExited: PropTypes.func,
Expand Down
8 changes: 7 additions & 1 deletion src/ReplaceTransition.js
Expand Up @@ -28,7 +28,13 @@ class ReplaceTransition extends React.Component {
const child = React.Children.toArray(children)[idx];

if (child.props[handler]) child.props[handler](...originalArgs)
if (this.props[handler]) this.props[handler](ReactDOM.findDOMNode(this))
if (this.props[handler]) {
const maybeNode = child.props.nodeRef
? undefined
: ReactDOM.findDOMNode(this)

this.props[handler](maybeNode)
}
}

render() {
Expand Down
76 changes: 58 additions & 18 deletions src/Transition.js
Expand Up @@ -210,65 +210,71 @@ class Transition extends React.Component {
if (nextStatus !== null) {
// nextStatus will always be ENTERING or EXITING.
this.cancelNextCallback()
const node = ReactDOM.findDOMNode(this)

if (nextStatus === ENTERING) {
this.performEnter(node, mounting)
this.performEnter(mounting)
} else {
this.performExit(node)
this.performExit()
}
} else if (this.props.unmountOnExit && this.state.status === EXITED) {
this.setState({ status: UNMOUNTED })
}
}

performEnter(node, mounting) {
performEnter(mounting) {
const { enter } = this.props
const appearing = this.context ? this.context.isMounting : mounting
const [maybeNode, maybeAppearing] = this.props.nodeRef
? [appearing]
: [ReactDOM.findDOMNode(this), appearing]

const timeouts = this.getTimeouts()
const enterTimeout = appearing ? timeouts.appear : timeouts.enter
// no enter animation skip right to ENTERED
// if we are mounting and running this it means appear _must_ be set
if ((!mounting && !enter) || config.disabled) {
this.safeSetState({ status: ENTERED }, () => {
this.props.onEntered(node)
this.props.onEntered(maybeNode)
})
return
}

this.props.onEnter(node, appearing)
this.props.onEnter(maybeNode, maybeAppearing)

this.safeSetState({ status: ENTERING }, () => {
this.props.onEntering(node, appearing)
this.props.onEntering(maybeNode, maybeAppearing)

this.onTransitionEnd(node, enterTimeout, () => {
this.onTransitionEnd(enterTimeout, () => {
this.safeSetState({ status: ENTERED }, () => {
this.props.onEntered(node, appearing)
this.props.onEntered(maybeNode, maybeAppearing)
})
})
})
}

performExit(node) {
performExit() {
const { exit } = this.props
const timeouts = this.getTimeouts()
const maybeNode = this.props.nodeRef
? undefined
: ReactDOM.findDOMNode(this)

// no exit animation skip right to EXITED
if (!exit || config.disabled) {
this.safeSetState({ status: EXITED }, () => {
this.props.onExited(node)
this.props.onExited(maybeNode)
})
return
}
this.props.onExit(node)

this.props.onExit(maybeNode)

this.safeSetState({ status: EXITING }, () => {
this.props.onExiting(node)
this.props.onExiting(maybeNode)

this.onTransitionEnd(node, timeouts.exit, () => {
this.onTransitionEnd(timeouts.exit, () => {
this.safeSetState({ status: EXITED }, () => {
this.props.onExited(node)
this.props.onExited(maybeNode)
})
})
})
Expand Down Expand Up @@ -308,8 +314,11 @@ class Transition extends React.Component {
return this.nextCallback
}

onTransitionEnd(node, timeout, handler) {
onTransitionEnd(timeout, handler) {
this.setNextCallback(handler)
const node = this.props.nodeRef
? this.props.nodeRef.current
: ReactDOM.findDOMNode(this)

const doesNotHaveTimeoutOrListener =
timeout == null && !this.props.addEndListener
Expand All @@ -319,7 +328,10 @@ class Transition extends React.Component {
}

if (this.props.addEndListener) {
this.props.addEndListener(node, this.nextCallback)
const [maybeNode, maybeNextCallback] = this.props.nodeRef
? [this.nextCallback]
: [node, this.nextCallback]
this.props.addEndListener(maybeNode, maybeNextCallback)
}

if (timeout != null) {
Expand Down Expand Up @@ -349,6 +361,7 @@ class Transition extends React.Component {
delete childProps.onExit
delete childProps.onExiting
delete childProps.onExited
delete childProps.nodeRef

if (typeof children === 'function') {
// allows for nested Transitions
Expand All @@ -370,6 +383,19 @@ class Transition extends React.Component {
}

Transition.propTypes = {
/**
* A React reference to DOM element that need to transition:
* https://stackoverflow.com/a/51127130/4671932
*
* - When `nodeRef` prop is used, `node` is not passed to callback functions
* (e.g. `onEnter`) because user already has direct access to the node.
* - When changing `key` prop of `Transition` in a `TransitionGroup` a new
* `nodeRef` need to be provided to `Transition` with changed `key` prop
* (see
* [test/CSSTransition-test.js](https://github.com/reactjs/react-transition-group/blob/13435f897b3ab71f6e19d724f145596f5910581c/test/CSSTransition-test.js#L362-L437)).
*/
nodeRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }),

/**
* A `function` child can be used instead of a React element. This function is
* called with the current transition status (`'entering'`, `'entered'`,
Expand Down Expand Up @@ -466,7 +492,9 @@ Transition.propTypes = {
/**
* Add a custom transition end trigger. Called with the transitioning
* DOM node and a `done` callback. Allows for more fine grained transition end
* logic. **Note:** Timeouts are still used as a fallback if provided.
* logic. Timeouts are still used as a fallback if provided.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* ```jsx
* addEndListener={(node, done) => {
Expand All @@ -481,6 +509,8 @@ Transition.propTypes = {
* Callback fired before the "entering" status is applied. An extra parameter
* `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* @type Function(node: HtmlElement, isAppearing: bool) -> void
*/
onEnter: PropTypes.func,
Expand All @@ -489,6 +519,8 @@ Transition.propTypes = {
* Callback fired after the "entering" status is applied. An extra parameter
* `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* @type Function(node: HtmlElement, isAppearing: bool)
*/
onEntering: PropTypes.func,
Expand All @@ -497,27 +529,35 @@ Transition.propTypes = {
* Callback fired after the "entered" status is applied. An extra parameter
* `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* @type Function(node: HtmlElement, isAppearing: bool) -> void
*/
onEntered: PropTypes.func,

/**
* Callback fired before the "exiting" status is applied.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* @type Function(node: HtmlElement) -> void
*/
onExit: PropTypes.func,

/**
* Callback fired after the "exiting" status is applied.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
*
* @type Function(node: HtmlElement) -> void
*/
onExiting: PropTypes.func,

/**
* Callback fired after the "exited" status is applied.
*
* **Note**: when `nodeRef` prop is passed, `node` is not passed
*
* @type Function(node: HtmlElement) -> void
*/
onExited: PropTypes.func,
Expand Down
1 change: 1 addition & 0 deletions src/TransitionGroup.js
Expand Up @@ -66,6 +66,7 @@ class TransitionGroup extends React.Component {
}
}

// node is `undefined` when user provided `nodeRef` prop
handleExited(child, node) {
let currentChildMapping = getChildMapping(this.props.children)

Expand Down

0 comments on commit 85016bf

Please sign in to comment.