I'm attempting to implement container components in React and Redux, and I'm unsure of what should take responsibility for lifecycle methods; containers or presentational components. One could argue that the lifecycle methods are presentational as they control DOM updates, but in that respect, aren't they also behavioural?
Furthermore, all of the implementations of container components that I've seen thus far utilise the react-redux
bindings, as do my own. Even if I keep the concerns clearly separated, is it appropriate to inherit from React.Component
in the case of a behaviour component?
For example, the app on which I'm working has a Tab
presentational component, with a shouldComponentUpdate
method:
class Tabs extends Component {
shouldComponentUpdate(nextProps) {
const { activeTab } = this.props;
return activeTab !== nextProps.activeTab;
}
[...]
}
On the one hand, this seems like a presentational concern as it controls when component should re-render. On the other hand, however, this is a means of handling when the user clicks a new tab, updating the application's state via an action, thus I'd class this as behavioural.
Data should be controlled as close to the root of the tree as possible. Doing this provides some simple optimizations, being that you're only passing what you need.
This will bubble down to where you are controlling some lifecycle components. As mgmcdermott mentioned, a lot of lifecycle components really depend on what you're doing, but the best case scenario is to have the simplest, dumbest components.
In most of my projects, in my react directory, I have components/
and views/
. It is always my preference that a view should do as much of the grunt work as possible. That being said, there a a number of components that I've built that use lifecycle methods like componentDidMount
, componentWillMount
, componentWillUnmount
, but I typically try and isolate updates in my views, since one of their jobs, in my opinion, is controlling data flow. That means componentShouldUpdate
would live there. Personally, I think componentShouldUpdate
is purely end-of-the-line optimization, though, and I only use it in cases where I'm having large performance issues during a re-render.
I'm not super sure I understand your "inherit from React.Component
" question. If you're asking whether or not to use pure functions, es6 class
, or React.createClass
, I don't know that there is a standard rule, but it is good to be consistent.
To address whether or not you are dealing with a behaviour or presentation, behaviour is the click, but re-drawing is presentation. Your behaviour might be well off to exist in your Tab
component, where the re-draw in your Tabs
view. Tabs
view passes your method from redux to set the currently active tab into your individual Tab
components, and can then send the behaviour of tab switching through redux so you can do your presentation componentShouldUpdate
. Does that make sense?
So your mapToDispatch
method in your container will have a function to set your active tab, let's call it activateTab(idx)
, which takes a 0-based index of the tab. Your container passes that to the containing component that you control, which is views/Tabs
, and it passes that method along to components/Tab
. components/Tab
will have an onClick
method which is listening on one of your DOM elements, which then calls this.props.activateTab(myIndex)
(you could also pass a bound version of activateTab into components/Tab
so it does not have to be aware of it's own index), which triggers redux, then passes back your data into views/Tabs
which can handle a componentShouldUpdate
based on the data from redux.
Expanded Edit: Since this was marked as accepted, I'll blow out my code example into something usable to the average person.
As a quick aside, I'm not going to write much redux, as this can be very app dependent, but I'm assuming that you have a state with activeTabIdx
hanging off the parent.
containers/TabExample.jsx
import { connect } from 'react-redux'
import Tabs from 'views/Tabs.js'
const mapStateToProps = function (state) {
return {
activeTabIdx: state.activeTabIdx
// And whatever else you have...
}
}
const mapDispatchToProps = function (dispatch) {
return {
activateTab: function (idx) {
dispatch({
action: 'ACTIVATE_TAB_IDX',
idx: idx
}) // You probably want this in a separate actions/tabs.js file...
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Tabs)
views/Tabs.js
import React, { createClass } from 'react'
import Tab from 'components/Tab.js'
const { number, func } = React.PropTypes
// Alternatively, you can use es6 classes...
export default createClass({
propTypes: {
activeTabIdx: number.isRequired,
activateTab: func.isRequired
},
render () {
const { activeTabIdx } = this.props
const tabs = ['Tab 1', 'Tab 2', 'Tab 3']
return (
<div className='view__tabs'>
<ol className='tabs'>
{this.renderTabLinks(tabs, activeTabIdx)}
</ol>
</div>
)
},
renderTabLinks (tabs, activeTabIdx) {
return tabs.map((tab, idx) => {
return (
<Tab
onClick={this.props.activateTabIdx.bind(this, idx)}
isActive={idx === activeTabIdx}
>
{tab}
</Tab>
)
})
}
})
components/Tab.js
import React, { createClass } from 'react'
const { func, bool } = React.PropTypes
// Alternatively, you can use es6 classes...
export default createClass({
propTypes: {
children: node.isRequired,
onClick: func.isRequired,
isActive: bool.isRequired
},
handleClick (e) {
const { isActive, onClick } = this.props
e.preventDefault()
if (!isActive) {
onClick()
}
},
render () {
const { children, isActive } = this.props
const tabClass = isActive
? 'tabs__items tabs__items--active'
: 'tabs__items'
return (
<li className={tabClass}>
<a className='tabs__item-link' onClick={this.handleClick}>
{children}
</a>
</li>
)
}
That will mostly do the right thing. Keep in mind that this doesn't handle/care about tab content, and as a result, you may want to structure your view differently.