React: Referencing Dynamic Children
I just started with React and decided that in order to familiarize myself with how things work I would create something useful. Talking with Ryan Florence he suggested making a tabs component. This turned out to be a great project in terms of challenging enough to really dig into React, but not so challenging that I was too focused on technical roadblocks to really spend time with React.
Composite Components
In React everything is a component. Like LegosĀ®, where you put smaller pieces together to create larger pieces, you can use components to create composite components which can be used to create something like an app. A composite component is a component comprised of other components.
For creating a tabs component I ended up with four different components: <Tabs/>
, <TabList/>
, <Tab/>
, and <TabPanel/>
. The hierarchy looks like this:
<Tabs>
<TabList>
<Tab>Foo</Tab>
<Tab>Bar</Tab>
</TabList>
<TabPanel>Content for Foo</TabPanel>
<TabPanel>Content for Bar</TabPanel>
</Tabs>
The <Tabs/>
component acts as the main container as well as the controller. It handles all the DOM events (clicking tab to select, arrow keys to select), it also manages the state for which <Tab/>
is currently selected.
The first challenge that I encountered was figuring out how components communicate with each other. Each component has a state
. Anytime the state
changes React will update the rendered component to reflect the state
. When the selected index changes the <Tabs/>
component needs to be able to update the state
of the appropriate <Tab/>
and <TabPanel/>
.
Paradigm Shift
The first thing I did was create an API for each component, exposing methods to mutate the state
. In React a component has access to it's children using this.props.children
. So initially I just called setSelected
on the appropriate child:
var tabs = this.props.children[0].props.children,
panels = this.props.children.slice(1),
index = this.state.selectedIndex;
tabs[index].setSelected(true);
panels[index].setSelected(true);
This actually worked, except that in React 0.10.0 it raised a warning:
Invalid access to component property "setSelected"
In React 0.11.0 it's even worse and throws an Error
.
What I didn't realize is that in React this.props.children
are descriptors, not the actual instances of the components. You should not reference children the way that I was using them.
Component Refs
React provides a mechanism for referencing the actual component instance by using the refs
property:
var App = React.createClass({
handleClick: function () {
alert(this.refs.myInput.getDOMNode().value);
},
render: function () {
return (
<div>
<input ref="myInput"/>
<button
onClick={this.handleClick}>
Submit
</button>
</div>
);
}
});
By using the refs
property you can gain reference to any child component.
Dynamic Children
The typical use case for refs
is that the parent component is already aware of what it's children are and just needs to gain reference to them. In my case using refs
wasn't so straight forward. I am supplying a component for other developers to use and they may add any number of <Tab/>
s. I expect that the <Tab/>
s will exist, but I am simply injecting what another developer has declared:
var App = React.createClass({
render: function () {
return (
<div>
{this.props.children}
</div>
);
}
});
I needed to find a way to dynamically specify my refs
. After some digging I discovered that React offers an addon that provides a way to solve this. Here's a simplified version of how I accomplished this:
var App = React.createClass({
render: function () {
var index = 0,
children = React.Children.map(this.props.children, function (child) {
return React.addons.cloneWithProps(child, {
ref: 'child-' + (index++)
});
});
return (
<div>
{children}
</div>
);
}
});
Now all the children have a ref
property and I can reference the dynamic children by using the refs
property:
var index = this.state.selectedIndex;
this.refs['tabs-' + index].setSelected(true);
this.refs['panels-' + index].setSelected(true);
Example
For full context on how this is working using real code you can check out react-tabs.
Open source hacker. Community organizer. Co-organizer @ReactRally. Software Sommelier.