Skip to content

Commit 85016bf

Browse files
author
Andrew Luca
authored
feat: add nodeRef alternative instead of internal findDOMNode (#559)
`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.
1 parent 13435f8 commit 85016bf

19 files changed

+549
-326
lines changed

.storybook/config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { configure } from '@storybook/react';
1+
import { configure, addDecorator } from '@storybook/react';
2+
import React from 'react';
3+
4+
addDecorator(
5+
storyFn => <React.StrictMode>{storyFn()}</React.StrictMode>,
6+
)
27

38
function loadStories() {
49
require('../stories');

src/CSSTransition.js

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -90,61 +90,72 @@ class CSSTransition extends React.Component {
9090
exit: {},
9191
}
9292

93-
onEnter = (node, appearing) => {
93+
onEnter = (maybeNode, maybeAppearing) => {
94+
const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing)
9495
this.removeClasses(node, 'exit');
9596
this.addClass(node, appearing ? 'appear' : 'enter', 'base');
9697

9798
if (this.props.onEnter) {
98-
this.props.onEnter(node, appearing)
99+
this.props.onEnter(maybeNode, maybeAppearing)
99100
}
100101
}
101102

102-
onEntering = (node, appearing) => {
103+
onEntering = (maybeNode, maybeAppearing) => {
104+
const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing)
103105
const type = appearing ? 'appear' : 'enter';
104106
this.addClass(node, type, 'active')
105107

106108
if (this.props.onEntering) {
107-
this.props.onEntering(node, appearing)
109+
this.props.onEntering(maybeNode, maybeAppearing)
108110
}
109111
}
110112

111-
onEntered = (node, appearing) => {
113+
onEntered = (maybeNode, maybeAppearing) => {
114+
const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing)
112115
const type = appearing ? 'appear' : 'enter'
113116
this.removeClasses(node, type);
114117
this.addClass(node, type, 'done');
115118

116119
if (this.props.onEntered) {
117-
this.props.onEntered(node, appearing)
120+
this.props.onEntered(maybeNode, maybeAppearing)
118121
}
119122
}
120123

121-
onExit = (node) => {
124+
onExit = (maybeNode) => {
125+
const [node] = this.resolveArguments(maybeNode)
122126
this.removeClasses(node, 'appear');
123127
this.removeClasses(node, 'enter');
124128
this.addClass(node, 'exit', 'base')
125129

126130
if (this.props.onExit) {
127-
this.props.onExit(node)
131+
this.props.onExit(maybeNode)
128132
}
129133
}
130134

131-
onExiting = (node) => {
135+
onExiting = (maybeNode) => {
136+
const [node] = this.resolveArguments(maybeNode)
132137
this.addClass(node, 'exit', 'active')
133138

134139
if (this.props.onExiting) {
135-
this.props.onExiting(node)
140+
this.props.onExiting(maybeNode)
136141
}
137142
}
138143

139-
onExited = (node) => {
144+
onExited = (maybeNode) => {
145+
const [node] = this.resolveArguments(maybeNode)
140146
this.removeClasses(node, 'exit');
141147
this.addClass(node, 'exit', 'done');
142148

143149
if (this.props.onExited) {
144-
this.props.onExited(node)
150+
this.props.onExited(maybeNode)
145151
}
146152
}
147153

154+
// when prop `nodeRef` is provided `node` is excluded
155+
resolveArguments = (maybeNode, maybeAppearing) => this.props.nodeRef
156+
? [this.props.nodeRef.current, maybeNode] // here `maybeNode` is actually `appearing`
157+
: [maybeNode, maybeAppearing] // `findDOMNode` was used
158+
148159
getClassNames = (type) => {
149160
const { classNames } = this.props;
150161
const isStringClassNames = typeof classNames === 'string';
@@ -306,6 +317,8 @@ CSSTransition.propTypes = {
306317
* A `<Transition>` callback fired immediately after the 'enter' or 'appear' class is
307318
* applied.
308319
*
320+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
321+
*
309322
* @type Function(node: HtmlElement, isAppearing: bool)
310323
*/
311324
onEnter: PropTypes.func,
@@ -314,6 +327,8 @@ CSSTransition.propTypes = {
314327
* A `<Transition>` callback fired immediately after the 'enter-active' or
315328
* 'appear-active' class is applied.
316329
*
330+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
331+
*
317332
* @type Function(node: HtmlElement, isAppearing: bool)
318333
*/
319334
onEntering: PropTypes.func,
@@ -322,6 +337,8 @@ CSSTransition.propTypes = {
322337
* A `<Transition>` callback fired immediately after the 'enter' or
323338
* 'appear' classes are **removed** and the `done` class is added to the DOM node.
324339
*
340+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
341+
*
325342
* @type Function(node: HtmlElement, isAppearing: bool)
326343
*/
327344
onEntered: PropTypes.func,
@@ -330,13 +347,17 @@ CSSTransition.propTypes = {
330347
* A `<Transition>` callback fired immediately after the 'exit' class is
331348
* applied.
332349
*
350+
* **Note**: when `nodeRef` prop is passed, `node` is not passed
351+
*
333352
* @type Function(node: HtmlElement)
334353
*/
335354
onExit: PropTypes.func,
336355

337356
/**
338357
* A `<Transition>` callback fired immediately after the 'exit-active' is applied.
339358
*
359+
* **Note**: when `nodeRef` prop is passed, `node` is not passed
360+
*
340361
* @type Function(node: HtmlElement)
341362
*/
342363
onExiting: PropTypes.func,
@@ -345,6 +366,8 @@ CSSTransition.propTypes = {
345366
* A `<Transition>` callback fired immediately after the 'exit' classes
346367
* are **removed** and the `exit-done` class is added to the DOM node.
347368
*
369+
* **Note**: when `nodeRef` prop is passed, `node` is not passed
370+
*
348371
* @type Function(node: HtmlElement)
349372
*/
350373
onExited: PropTypes.func,

src/ReplaceTransition.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ class ReplaceTransition extends React.Component {
2828
const child = React.Children.toArray(children)[idx];
2929

3030
if (child.props[handler]) child.props[handler](...originalArgs)
31-
if (this.props[handler]) this.props[handler](ReactDOM.findDOMNode(this))
31+
if (this.props[handler]) {
32+
const maybeNode = child.props.nodeRef
33+
? undefined
34+
: ReactDOM.findDOMNode(this)
35+
36+
this.props[handler](maybeNode)
37+
}
3238
}
3339

3440
render() {

src/Transition.js

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -210,65 +210,71 @@ class Transition extends React.Component {
210210
if (nextStatus !== null) {
211211
// nextStatus will always be ENTERING or EXITING.
212212
this.cancelNextCallback()
213-
const node = ReactDOM.findDOMNode(this)
214213

215214
if (nextStatus === ENTERING) {
216-
this.performEnter(node, mounting)
215+
this.performEnter(mounting)
217216
} else {
218-
this.performExit(node)
217+
this.performExit()
219218
}
220219
} else if (this.props.unmountOnExit && this.state.status === EXITED) {
221220
this.setState({ status: UNMOUNTED })
222221
}
223222
}
224223

225-
performEnter(node, mounting) {
224+
performEnter(mounting) {
226225
const { enter } = this.props
227226
const appearing = this.context ? this.context.isMounting : mounting
227+
const [maybeNode, maybeAppearing] = this.props.nodeRef
228+
? [appearing]
229+
: [ReactDOM.findDOMNode(this), appearing]
228230

229231
const timeouts = this.getTimeouts()
230232
const enterTimeout = appearing ? timeouts.appear : timeouts.enter
231233
// no enter animation skip right to ENTERED
232234
// if we are mounting and running this it means appear _must_ be set
233235
if ((!mounting && !enter) || config.disabled) {
234236
this.safeSetState({ status: ENTERED }, () => {
235-
this.props.onEntered(node)
237+
this.props.onEntered(maybeNode)
236238
})
237239
return
238240
}
239241

240-
this.props.onEnter(node, appearing)
242+
this.props.onEnter(maybeNode, maybeAppearing)
241243

242244
this.safeSetState({ status: ENTERING }, () => {
243-
this.props.onEntering(node, appearing)
245+
this.props.onEntering(maybeNode, maybeAppearing)
244246

245-
this.onTransitionEnd(node, enterTimeout, () => {
247+
this.onTransitionEnd(enterTimeout, () => {
246248
this.safeSetState({ status: ENTERED }, () => {
247-
this.props.onEntered(node, appearing)
249+
this.props.onEntered(maybeNode, maybeAppearing)
248250
})
249251
})
250252
})
251253
}
252254

253-
performExit(node) {
255+
performExit() {
254256
const { exit } = this.props
255257
const timeouts = this.getTimeouts()
258+
const maybeNode = this.props.nodeRef
259+
? undefined
260+
: ReactDOM.findDOMNode(this)
256261

257262
// no exit animation skip right to EXITED
258263
if (!exit || config.disabled) {
259264
this.safeSetState({ status: EXITED }, () => {
260-
this.props.onExited(node)
265+
this.props.onExited(maybeNode)
261266
})
262267
return
263268
}
264-
this.props.onExit(node)
269+
270+
this.props.onExit(maybeNode)
265271

266272
this.safeSetState({ status: EXITING }, () => {
267-
this.props.onExiting(node)
273+
this.props.onExiting(maybeNode)
268274

269-
this.onTransitionEnd(node, timeouts.exit, () => {
275+
this.onTransitionEnd(timeouts.exit, () => {
270276
this.safeSetState({ status: EXITED }, () => {
271-
this.props.onExited(node)
277+
this.props.onExited(maybeNode)
272278
})
273279
})
274280
})
@@ -308,8 +314,11 @@ class Transition extends React.Component {
308314
return this.nextCallback
309315
}
310316

311-
onTransitionEnd(node, timeout, handler) {
317+
onTransitionEnd(timeout, handler) {
312318
this.setNextCallback(handler)
319+
const node = this.props.nodeRef
320+
? this.props.nodeRef.current
321+
: ReactDOM.findDOMNode(this)
313322

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

321330
if (this.props.addEndListener) {
322-
this.props.addEndListener(node, this.nextCallback)
331+
const [maybeNode, maybeNextCallback] = this.props.nodeRef
332+
? [this.nextCallback]
333+
: [node, this.nextCallback]
334+
this.props.addEndListener(maybeNode, maybeNextCallback)
323335
}
324336

325337
if (timeout != null) {
@@ -349,6 +361,7 @@ class Transition extends React.Component {
349361
delete childProps.onExit
350362
delete childProps.onExiting
351363
delete childProps.onExited
364+
delete childProps.nodeRef
352365

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

372385
Transition.propTypes = {
386+
/**
387+
* A React reference to DOM element that need to transition:
388+
* https://stackoverflow.com/a/51127130/4671932
389+
*
390+
* - When `nodeRef` prop is used, `node` is not passed to callback functions
391+
* (e.g. `onEnter`) because user already has direct access to the node.
392+
* - When changing `key` prop of `Transition` in a `TransitionGroup` a new
393+
* `nodeRef` need to be provided to `Transition` with changed `key` prop
394+
* (see
395+
* [test/CSSTransition-test.js](https://github.com/reactjs/react-transition-group/blob/13435f897b3ab71f6e19d724f145596f5910581c/test/CSSTransition-test.js#L362-L437)).
396+
*/
397+
nodeRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
398+
373399
/**
374400
* A `function` child can be used instead of a React element. This function is
375401
* called with the current transition status (`'entering'`, `'entered'`,
@@ -466,7 +492,9 @@ Transition.propTypes = {
466492
/**
467493
* Add a custom transition end trigger. Called with the transitioning
468494
* DOM node and a `done` callback. Allows for more fine grained transition end
469-
* logic. **Note:** Timeouts are still used as a fallback if provided.
495+
* logic. Timeouts are still used as a fallback if provided.
496+
*
497+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
470498
*
471499
* ```jsx
472500
* addEndListener={(node, done) => {
@@ -481,6 +509,8 @@ Transition.propTypes = {
481509
* Callback fired before the "entering" status is applied. An extra parameter
482510
* `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
483511
*
512+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
513+
*
484514
* @type Function(node: HtmlElement, isAppearing: bool) -> void
485515
*/
486516
onEnter: PropTypes.func,
@@ -489,6 +519,8 @@ Transition.propTypes = {
489519
* Callback fired after the "entering" status is applied. An extra parameter
490520
* `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
491521
*
522+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
523+
*
492524
* @type Function(node: HtmlElement, isAppearing: bool)
493525
*/
494526
onEntering: PropTypes.func,
@@ -497,27 +529,35 @@ Transition.propTypes = {
497529
* Callback fired after the "entered" status is applied. An extra parameter
498530
* `isAppearing` is supplied to indicate if the enter stage is occurring on the initial mount
499531
*
532+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
533+
*
500534
* @type Function(node: HtmlElement, isAppearing: bool) -> void
501535
*/
502536
onEntered: PropTypes.func,
503537

504538
/**
505539
* Callback fired before the "exiting" status is applied.
506540
*
541+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
542+
*
507543
* @type Function(node: HtmlElement) -> void
508544
*/
509545
onExit: PropTypes.func,
510546

511547
/**
512548
* Callback fired after the "exiting" status is applied.
513549
*
550+
* **Note**: when `nodeRef` prop is passed, `node` is not passed.
551+
*
514552
* @type Function(node: HtmlElement) -> void
515553
*/
516554
onExiting: PropTypes.func,
517555

518556
/**
519557
* Callback fired after the "exited" status is applied.
520558
*
559+
* **Note**: when `nodeRef` prop is passed, `node` is not passed
560+
*
521561
* @type Function(node: HtmlElement) -> void
522562
*/
523563
onExited: PropTypes.func,

src/TransitionGroup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class TransitionGroup extends React.Component {
6666
}
6767
}
6868

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

0 commit comments

Comments
 (0)