Codebase deep dive: ReactTransitionGroup
This week, I needed to read through the code of ReactTransitionGroup
in order to modify it for one of my apps at work.
Here is my commentary on it.
This is the link1 to the GitHub repo.
The maintenance and further development of ReactTransitionGroup
will be transferred to the community because a lack of use cases has caused the package to be neglected by the React core team.
ReactTransitionGroup
will be removed from the React core in v15.5.
ReactTransitionGroup
In addition to the explicit component
prop and implicit children
prop, a ReactTransitionGroup
also takes an undocumented childFactory
prop, which allows you to modify each child element before rendering it to the DOM.
childFactory
defaults to the identity function in ReactTransitionGroup
but is used by the higher-level ReactCSSTransitionGroup
to put each child inside a wrapper element that adds or removes appropriate CSS classes when that child enter or exits the DOM.
The transition group uses an internal childRefs
property to keep track of which children is actually mounted in the real DOM at any one time.
This includes elements that are entering, staying and leaving.
The group also uses a currentlyTransitioningKeys
to keep track of which children are being animated in or out of the DOM.
When a ReactTransitionGroup
has already been mounted in the DOM, changing from one set of children to another set involves the following steps, carried out by the componentWillReceiveProps
method (reproduced below):
Immediately, React try to figure out how to merge the old and new set of children in a way that causes the least amount of disruption to their ordering in the actual DOM by calling
mergeChildMappings
in theChildMappings
utility module. Miminizing changes to the ordering of DOM elements is necessary to minimize any mounting and unmounting of DOM elements, which will disrupt any CSS transitions for those DOM elements.React then renders this merged group of children into the DOM. Elements that were already in the DOM will of course not experience any change because their
key
s stay the same.Then React divides this set of elements into three groups and animate them using two
for
loops2:- Those that wasn’t in the previous
props.children
are added tothis.keysToEnter
property so that they’ll receive theenter
transition. - Those that aren’t in the current
props.children
are added tothis.keysToLeave
property so that they’ll receive theleave
transition. - Those that stay receive no special treatment because they will be handled appropriately by React’s own reconcilliation process.
- Those that wasn’t in the previous
1 componentWillReceiveProps(nextProps) {2 let nextChildMapping = getChildMapping(nextProps.children);3 let prevChildMapping = this.state.children;45 this.setState({6 children: mergeChildMappings(7 prevChildMapping,8 nextChildMapping,9 ),10 });1112 for (let key in nextChildMapping) {13 let hasPrev = prevChildMapping && prevChildMapping.hasOwnProperty(key);14 if (nextChildMapping[key] && !hasPrev &&15 !this.currentlyTransitioningKeys[key]) {16 this.keysToEnter.push(key);17 }18 }1920 for (let key in prevChildMapping) {21 let hasNext = nextChildMapping && nextChildMapping.hasOwnProperty(key);22 if (prevChildMapping[key] && !hasNext &&23 !this.currentlyTransitioningKeys[key]) {24 this.keysToLeave.push(key);25 }26 }2728 // If we want to someday check for reordering, we could do it here.29 }
Note that the accounting of which elements should leave or stay happens right after the setState
call and implicitly relies on the fact that setState
calls are asynchronous 3. Had setState
been synchronous, componentDidUpdate
would have been called between lines 55 and 57 and the transitions called in componentDidUpdate
wouldn’t have worked because this.keysToEnter
and this.keysToLeave
would be empty arrays inside componentDidUpdate
.
The newly merged state.children
are then rendered to the virtual DOM by render
.
This method transforms each child with the chidlFactory
prop.
It also uses React.cloneElement
to give each child element a ref
callback so that it adds itself to the the transition group’s childRefs
.
After the merged children
are flushed to the DOM, componentDidUpdate
triggers the enter
animation for each element in the array this.keysToEnter
and resets the array to empty. The same process happens for leave
ing elements.
performEnter
marks the input child React component as being transitioned and get ahold of the actual DOM element backing that component.
If the child element declares a componentWillEnter
method, that method will get invoked and passed the _handleDoneEntering
method as a callback argument.
If the child element doesn’t declare that method, _handleDoneAppearing
is executed immediately.
_handleDoneEntering
calls the child component’s componentDidEnter
method if it exists.
It then removes the child from the list of currentlyTransitioningKeys
.
It also nicely takes care of the case when the currently enter
ing element needs to be removed from the DOM before the transition is completed.
In that case, the leave
transtion is triggered.
_handleDoneLeaving
is very similar to _handleDoneEntering
except that is also update state.children
by removing the exiting child.
Note the callback form of setState
: setState(function(currentState, props) => newState)
instead of the more typical form setState(nextState)
.
This will trigger a re-render in the virtual DOM, which may or may not cause reconcilliation to happen right away.
It’s interesting that the code uses the delete
operator to remove an object’s property instead of null
ing it out.
Performance-wise, delete
ing an object is about 70% slower than null
ing.
The main reason has to do with hidden-classes used in the V8 engine.
Basically, delete
ing a property invalidates some of the assumptions the JavaScript engine makes about the shape of the transition group object in order to optimize property access.
Using null
instead of delete
wouldn’t have any adverse effect because React children that are null
won’t be rendered to the DOM. On the other hand, because ReactTransitionGroup
is supposed to serve pretty simple use cases, it usually doesn’t have a lot of children so this approach is probably fine.
ChildMapping
The utility functions in this helper module are pretty interesting:
getChildMapping
converts a list of React elements into key-value pairs where the keys are the user-defined React elements and the values are the corresponding elements. This is why the documentation saysYou must provide the key attribute for all children of
ReactCSSTransitionGroup
, even when only rendering a single item. This is how React will determine which children have entered, left, or stayed.If you forget the key, the element will “fall through the crack” and will not receive any enter/exit transition. Also note the use of the the
React.Children
API. You should not access thethis.props.children
directly because it is an opaque structure4 but rather use theReact.Children
API.getChildMapping
can also deal with the case where the input is falsy. This happens when the function is called from within a component that has already been unmounted e.g. from within the “done” callback ofcomponentWillAppear
andcomponentWillEnter
.
mergeChildMappings
interleaves the old with the new keys while aiming for a sensible order. The order is obtained by “lining up” the common keys between the old and new keys. Between the “uncommon keys”, the new keys are shown first followed by old ones. For the example given in this pen (the underscores represent white spaces):key-2_key-1_____________key-5_key-7_______key-8
(old keys)______key-1_key-3_key-6_______key-7_key-9______
( new keys)key-2_key-1_key-3_key-6_key-5_key-7_key-9_key-8
(merge result)An interesting consequence of this merge’s implicit reliance on ECMAScript’s ordering of an object’s keys is that if some of the keys are “numeric” e.g.
7
instead ofkey-7
, this merge method will give completely different results:1 2 3 4 5 6 7 8 9
The reason is that according to ES2015, all the numeric properties will be listed first in ascending order followed by other string keys. This order will be reflected in the DOM and may cause unintended behavior e.g. unnecessary repositioning between list items if you use the transition group to animate entry/exit of list items. This is also the reason why I use
key-1
instead of just1
in the demo.
Takeaways:
- Do not use “numeric” keys for children of React transition group children because it will mess up their ordering in the DOM.
- Always supply a
key
for each children of the transition group.
- Note that this isn’t the official repo from the core React team.↩
- This is similar to D3’s
enter
/update
/exit
pattern:↩ - The asynchronous nature of
setState
is important in React. The official documentation and this short writeup clearly mention that React schedules DOM updates instead of just simply flushes out all the DOMdiff
s that it can detect on every frame.↩ - The main reason is that there might be many, one or no children at all and this API provides some uniforminity in how to access the children. For a detailed discussion, see this Github issue.↩