Skip to content

Commit 41e0322

Browse files
committed
chore(TransitionGroup): use React.forwardRef() (#4266)
1 parent e32d6ce commit 41e0322

File tree

3 files changed

+96
-87
lines changed

3 files changed

+96
-87
lines changed

src/modules/Transition/Transition.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cx from 'clsx'
22
import _ from 'lodash'
33
import PropTypes from 'prop-types'
4-
import { cloneElement, Component } from 'react'
4+
import * as React from 'react'
55

66
import { makeDebugger, normalizeTransitionDuration, SUI, useKeyOnly } from '../../lib'
77
import TransitionGroup from './TransitionGroup'
@@ -29,15 +29,7 @@ const TRANSITION_STYLE_TYPE = {
2929
/**
3030
* A transition is an animation usually used to move content in or out of view.
3131
*/
32-
export default class Transition extends Component {
33-
/** @deprecated Static properties will be removed in v2. */
34-
static INITIAL = TRANSITION_STATUS_INITIAL
35-
static ENTERED = TRANSITION_STATUS_ENTERED
36-
static ENTERING = TRANSITION_STATUS_ENTERING
37-
static EXITED = TRANSITION_STATUS_EXITED
38-
static EXITING = TRANSITION_STATUS_EXITING
39-
static UNMOUNTED = TRANSITION_STATUS_UNMOUNTED
40-
32+
export default class Transition extends React.Component {
4133
static Group = TransitionGroup
4234

4335
state = {
@@ -171,7 +163,7 @@ export default class Transition extends Component {
171163
return null
172164
}
173165

174-
return cloneElement(children, {
166+
return React.cloneElement(children, {
175167
className: this.computeClasses(),
176168
style: this.computeStyle(),
177169
...(process.env.NODE_ENV !== 'production' && {

src/modules/Transition/TransitionGroup.js

Lines changed: 74 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,64 +2,66 @@ import _ from 'lodash'
22
import PropTypes from 'prop-types'
33
import React from 'react'
44

5-
import { getElementType, getUnhandledProps, makeDebugger, SUI } from '../../lib'
5+
import { getElementType, getUnhandledProps, makeDebugger, SUI, useEventCallback } from '../../lib'
66
import { getChildMapping, mergeChildMappings } from './utils/childMapping'
77
import wrapChild from './utils/wrapChild'
88

99
const debug = makeDebugger('transition_group')
1010

1111
/**
12-
* A Transition.Group animates children as they mount and unmount.
12+
* Wraps all children elements with proper callbacks and props.
13+
*
14+
* @param {React.ReactNode} children
15+
* @param {Stream} animation
16+
* @param {Number|String|Object} duration
17+
* @param {Boolean} directional
18+
*
19+
* @return {Object}
1320
*/
14-
export default class TransitionGroup extends React.Component {
15-
state = {
16-
// Keeping a callback under the state is a hack to make it accessible under getDerivedStateFromProps()
17-
handleOnHide: (nothing, childProps) => {
18-
debug('handleOnHide', childProps)
19-
const { reactKey } = childProps
20-
21-
this.setState((state) => {
22-
const children = { ...state.children }
23-
delete children[reactKey]
24-
25-
return { children }
26-
})
27-
},
28-
}
21+
function useWrappedChildren(children, animation, duration, directional) {
22+
debug('wrapChildren()')
2923

30-
static getDerivedStateFromProps(props, state) {
31-
debug('getDerivedStateFromProps()')
32-
33-
const { animation, duration, directional } = props
34-
const { children: prevMapping } = state
35-
36-
// A short circuit for an initial render as there will be no `prevMapping`
37-
if (typeof prevMapping === 'undefined') {
38-
return {
39-
children: _.mapValues(getChildMapping(props.children), (child) =>
40-
wrapChild(child, state.handleOnHide, {
41-
animation,
42-
duration,
43-
directional,
44-
}),
45-
),
46-
}
47-
}
24+
const [, forceUpdate] = React.useReducer((x) => x + 1, 0)
25+
26+
const previousChildren = React.useRef()
27+
let wrappedChildren
4828

49-
const nextMapping = getChildMapping(props.children)
50-
const children = mergeChildMappings(prevMapping, nextMapping)
29+
React.useEffect(() => {
30+
previousChildren.current = wrappedChildren
31+
})
5132

52-
_.forEach(children, (child, key) => {
53-
const hasPrev = _.has(prevMapping, key)
54-
const hasNext = _.has(nextMapping, key)
33+
const handleChildHide = useEventCallback((nothing, childProps) => {
34+
debug('handleOnHide', childProps)
35+
const { reactKey } = childProps
36+
37+
delete previousChildren.current[reactKey]
38+
forceUpdate()
39+
})
40+
41+
// A short circuit for an initial render as there will be no `prevMapping`
42+
if (typeof previousChildren.current === 'undefined') {
43+
wrappedChildren = _.mapValues(getChildMapping(children), (child) =>
44+
wrapChild(child, handleChildHide, {
45+
animation,
46+
duration,
47+
directional,
48+
}),
49+
)
50+
} else {
51+
const nextMapping = getChildMapping(children)
52+
wrappedChildren = mergeChildMappings(previousChildren.current, nextMapping)
5553

56-
const { [key]: prevChild } = prevMapping
54+
_.forEach(wrappedChildren, (child, key) => {
55+
const hasPrev = previousChildren.current[key]
56+
const hasNext = nextMapping[key]
57+
58+
const prevChild = previousChildren.current[key]
5759
const isLeaving = !_.get(prevChild, 'props.visible')
5860

5961
// Heads up!
6062
// An item is new (entering), it will be picked from `nextChildren`, so it should be wrapped
6163
if (hasNext && (!hasPrev || isLeaving)) {
62-
children[key] = wrapChild(child, state.handleOnHide, {
64+
wrappedChildren[key] = wrapChild(child, handleChildHide, {
6365
animation,
6466
duration,
6567
directional,
@@ -72,7 +74,7 @@ export default class TransitionGroup extends React.Component {
7274
// An item is old (exiting), it will be picked from `prevChildren`, so it has been already
7375
// wrapped, so should be only updated
7476
if (!hasNext && hasPrev && !isLeaving) {
75-
children[key] = React.cloneElement(prevChild, { visible: false })
77+
wrappedChildren[key] = React.cloneElement(prevChild, { visible: false })
7678
return
7779
}
7880

@@ -83,31 +85,44 @@ export default class TransitionGroup extends React.Component {
8385
props: { visible, transitionOnMount },
8486
} = prevChild
8587

86-
children[key] = wrapChild(child, state.handleOnHide, {
88+
wrappedChildren[key] = wrapChild(child, handleChildHide, {
8789
animation,
8890
duration,
8991
directional,
9092
transitionOnMount,
9193
visible,
9294
})
9395
})
94-
95-
return { children }
9696
}
9797

98-
render() {
99-
debug('render')
100-
debug('props', this.props)
101-
debug('state', this.state)
102-
103-
const { children } = this.state
104-
const ElementType = getElementType(TransitionGroup, this.props)
105-
const rest = getUnhandledProps(TransitionGroup, this.props)
106-
107-
return <ElementType {...rest}>{_.values(children)}</ElementType>
108-
}
98+
return wrappedChildren
10999
}
110100

101+
/**
102+
* A Transition.Group animates children as they mount and unmount.
103+
*/
104+
const TransitionGroup = React.forwardRef(function (props, ref) {
105+
debug('render')
106+
debug('props', props)
107+
108+
const children = useWrappedChildren(
109+
props.children,
110+
props.animation,
111+
props.duration,
112+
props.directional,
113+
)
114+
115+
const ElementType = getElementType(TransitionGroup, props)
116+
const rest = getUnhandledProps(TransitionGroup, props)
117+
118+
return (
119+
<ElementType {...rest} ref={ref}>
120+
{_.values(children)}
121+
</ElementType>
122+
)
123+
})
124+
125+
TransitionGroup.displayName = 'TransitionGroup'
111126
TransitionGroup.propTypes = {
112127
/** An element type to render as (string or function). */
113128
as: PropTypes.elementType,
@@ -137,3 +152,5 @@ TransitionGroup.defaultProps = {
137152
animation: 'fade',
138153
duration: 500,
139154
}
155+
156+
export default TransitionGroup

test/specs/modules/Transition/TransitionGroup-test.js

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import * as common from 'test/specs/commonTests'
77
let wrapper
88

99
const wrapperMount = (...args) => (wrapper = mount(...args))
10-
const wrapperShallow = (...args) => (wrapper = shallow(...args))
1110

1211
describe('TransitionGroup', () => {
1312
common.isConformant(TransitionGroup, {
1413
rendersFragmentByDefault: true,
1514
rendersChildren: false,
1615
})
16+
common.forwardsRef(TransitionGroup, { requiredProps: { as: 'div' } })
1717

1818
beforeEach(() => {
1919
wrapper = undefined
@@ -25,50 +25,50 @@ describe('TransitionGroup', () => {
2525

2626
describe('children', () => {
2727
it('wraps all children to Transition', () => {
28-
shallow(
28+
wrapperMount(
2929
<TransitionGroup>
3030
<div />
3131
<div />
3232
<div />
3333
</TransitionGroup>,
3434
)
35-
.children()
36-
.everyWhere((item) => item.type().should.equal(Transition))
35+
36+
wrapper.children().everyWhere((item) => item.type().should.equal(Transition))
3737
})
3838

3939
it('passes props to children', () => {
40-
shallow(
40+
wrapperMount(
4141
<TransitionGroup animation='scale' directional duration={1500}>
4242
<div />
4343
<div />
4444
<div />
4545
</TransitionGroup>,
4646
)
47-
.children()
48-
.everyWhere((item) => {
49-
item.should.have.prop('animation', 'scale')
50-
item.should.have.prop('directional', true)
51-
item.should.have.prop('duration', 1500)
52-
item.type().should.equal(Transition)
53-
})
47+
48+
wrapper.children().everyWhere((item) => {
49+
item.should.have.prop('animation', 'scale')
50+
item.should.have.prop('directional', true)
51+
item.should.have.prop('duration', 1500)
52+
item.type().should.equal(Transition)
53+
})
5454
})
5555

5656
it('wraps new child to Transition and sets transitionOnMount to true', () => {
57-
wrapperShallow(
57+
wrapperMount(
5858
<TransitionGroup>
5959
<div key='first' />
6060
</TransitionGroup>,
6161
)
6262
wrapper.setProps({ children: [<div key='first' />, <div key='second' />] })
6363

64-
const child = wrapper.childAt(1)
65-
child.key().should.equal('.$second')
66-
child.type().should.equal(Transition)
67-
child.should.have.prop('transitionOnMount', true)
64+
const secondChild = wrapper.childAt(1)
65+
secondChild.key().should.equal('.$second')
66+
secondChild.type().should.equal(Transition)
67+
secondChild.should.have.prop('transitionOnMount', true)
6868
})
6969

7070
it('skips invalid children', () => {
71-
wrapperShallow(
71+
wrapperMount(
7272
<TransitionGroup>
7373
<div key='first' />
7474
</TransitionGroup>,
@@ -81,7 +81,7 @@ describe('TransitionGroup', () => {
8181
})
8282

8383
it('sets visible to false when child was removed', () => {
84-
wrapperShallow(
84+
wrapperMount(
8585
<TransitionGroup>
8686
<div key='first' />
8787
<div key='second' />

0 commit comments

Comments
 (0)