Reputation: 1213
am refactorizing huge project in which
I've thought and searched for ways to handle this issue here and there and found this great article about Higher Order Components (HOC) - basically being components that wrap another components.
https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.8vr464t20
I will now give you (A) example of two out of eight similar components types that I need to handle, than (B) I will paste the code I came up with that unifies those eight files into one. Finally (C) will paste example of usage of that unified component.
I will try to be consistent and naming will be not domain driven (I can't post project details here) but same names in different code excerpts below will always point to same components and data. Otherwise I will point it.
(A) - similar components types
1. TabA - Simple one
export default class TabA extends Component {
render() {
return (
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something={this.props.something }/>
</GridItem>
<GridItem xsSize="9">
<Tabs
permalink = { this.props.permalink }
history={ this.props.history }
activeTab={ Paths.somePath }
/>
<TabAContent
data={ this.props.data }
name={ this.props.name }
someValue={ this.props.someValue }
/>
</GridItem>
</Grid>
</PageWrapper>
);
}
}
Notice that SomeComponentA does not take any children in. Also there is no conditional rendering of any kind here.
2. TabB - More complex one
Similarly here, notice that renderSomeData method conditionally renders SmartComponentToBeConditionallyRendered and also SomeComponentB takes children in from the props.
export default class TabB extends Component {
renderSomeData() {
let someData = {
header: "Header text",
searchPlaceHolder: 'Search (name)',
buttonCaption: 'button caption'
};
return (
<SmartComponentToBeConditionallyRendered
type={ 'some_type' }
permalink={ this.props.permalink }
data={ someData }
/>
)
}
render() {
let { data } = this.props;
return (
<div>
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something={this.props.something}/>
</GridItem>
<GridItem xsSize="9">
<Tabs
permalink = { this.props.permalink }
history = { this.props.history }
activeTab = { Paths.somePage }
/>
<TabBContent data = { data }>
{this.props.children}
</TabBContent>
</GridItem>
</Grid>
</PageWrapper>
{
this.context.hasPermission('somePermission') ?
this.renderSomeData() :
null
}
</div>
)
}
static contextTypes = {
hasPermission: React.PropTypes.func.isRequired
}
}
Those eight components I've wrote about at the beginning - they all represent one of three possibilities. Two pictured above and possibility C, but differences in C are just another conditionally rendered components so not worth mentioning cause it will finally come down to passing more flags in props.
Those two components above - they differ in:
(B) What I came up with
let availablePartials = {
PartialA: PartialA,
PartialB: PartialB,
PartialC: PartialC
}
export default class GenericTab extends Component {
renderSomeData() {
return (
<SomeData
type = { this.props.type }
permalink = { this.props.permalink }
data = { this.props.someData } //PASSED FROM PROPS NOW
/>
);
}
render() {
let tabContent = React.createElement(
availablePartials[this.props.partialView.name],
this.props.partialView.props,
this.props.renderChildren ? this.props.children : null
);
return (
<div>
<PageWrapper>
<Grid>
<GridItem xsSize="3">
<SmartComponent something = { this.props.permalink }/>
</GridItem>
<GridItem xsSize = "9">
<Tabs
permalink = { this.props.permalink }
history = { this.props.history }
activeTab = { this.props.activeTab }
/>
{ tabContent }
</GridItem>
</Grid>
</PageWrapper>
{
this.context.hasPermission(this.props.requiredPermission) && this.props.dataForSomeDataMethod ?
this.renderSomeData()
: null
}
</div>
)
}
static contextTypes = {
hasPermission: React.PropTypes.func.isRequired
}
};
CityPageTab.propTypes = {
permalink: PropTypes.string,
dataForSomeDataMethod: PropTypes.object,
type: PropTypes.string,
activeTab: PropTypes.string,
renderChildren: PropTypes.bool,
partialView: PropTypes.object,
requiredPermission: PropTypes.string
};
Basically EVERYTHING is constructed from props. The only part I don't like is availablePartials[this.props.partialView.name]. It requires developers to keep the state of availablePartials object consistent and tangles it a bit. Not nice solution but still it is best I came up with so far.
(C) New GenericTab usage example
componentThatUseGenericTabRenderMethod() {
let { valueA, valueB, valueC, history } = this.props;
let someData = {
header: 'header text',
searchPlaceHolder: 'Search (name)',
buttonCaption: 'buttonCaption'
}
return (
<GenericTab
partialView = {{
name: 'PartialA',
props: {
A: valueA,
B: valueB,
C: valueC,
history: history,
permalink: this.props.params.permalink
}
}}
permalink = { this.props.params.permalink }
activeTab = { Paths.somePath }
someData = { someData }
type = { 'SOME_TYPE' }
renderChildren = { false }
requiredPermission = { 'some_required_permision' }
/>
);
}
So that is that. Usage got bit more complex, but I got rid of seven files (and getting rid of files is main objective as there is too many of them) and am going to further push it in similar manner - generic one. Thing with genericity - it is more difficult to use but saves lots of space.
Project utilises Redux so dont be too concerned about passing props down the tree. They always only come from some SmartParentComponent that renders GenericTab
Below is the visualisation of how it looks on the page. GenericTab is responsible for rendering Tabs and TabContent parts. Yes I know it is shitty solution, but am not responsible for architecture of it. There are so many things to be refactorized here and what am asking about is just a step in a journey. So please lets focus on the question asked and not other things that are so wrong with this code. I know.:)
Guess I could make an article out of it but I don't really have blog to do it:).
Please tell me what you think, propose upgrades, different ways of handling this problem etc.
Upvotes: 2
Views: 249
Reputation: 8686
When dealing with such architecture (i.e, tabs in your case), you basically don't want to hide the architecture under the hood, because in this case your ending up adding more and more properties with each new case you want to handle.
Instead, you wan't to let react handles the nested structure since it's where react really shines. That let you write something very generic by handling the built in children
props. You typically want to write something like :
const PageWithTabs = (props) => (
<Tabs defaultActive={'targaryens-panel'}>
<TabBar>
<TabLink href="#starks-panel">{'Starks'}</TabLink>
<TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
<TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
</TabBar>
<TabPanel id="starks-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Eddard</li>
<li>Catelyn</li>
<li>Robb</li>
<li>Sansa</li>
<li>Brandon</li>
<li>Arya</li>
<li>Rickon</li>
</ul>
</TabPanel>
<TabPanel id="lannisters-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Tywin</li>
<li>Cersei</li>
<li>Jamie</li>
<li>Tyrion</li>
</ul>
</TabPanel>
<TabPanel id="targaryens-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Viserys</li>
<li>Daenerys</li>
</ul>
</TabPanel>
</Tabs>
)
The point here is that you don't have to "predict" all the things that might appear under each TabPanel
, simply let the developper put whatever he wants ! BUT me need some logic to handle the "go to tab" sort of things.
React provides some very handy utilities methods to dynamically clone elements, map over elements, and render element whether its type is the one you expect or not (in our case, we expect TabBar
or TabPanel
type, nothing prevent the developper to put any other components than this two but nothing prevent him neither to put any built in <table>
html element inside of <a>
tag or something weird like that).
Here is a little implementation with Material Design Lite, it's not perfect but you should get the point :
class Tabs extends React.Component {
constructor(props) {
super(props),
this.state = {
activeTabId: props.defaultActive
}
}
tabClickHandlerFactory(id) {
return (e) => {
e.preventDefault()
this.setState({
activeTabId: id
})
}
}
getPanelIdFromLink(href) {
return href.split('#')[1]
}
render() {
const self = this
return (
<div className='mdl-tabs is-upgraded' {...self.props}>
{React.Children.map(self.props.children, (child, index) => {
if (child.type == TabBar) {
return React.cloneElement(child, {}, React.Children.map(child.props.children, (link) => {
const id = self.getPanelIdFromLink(link.props.href)
return (
React.cloneElement(link, {
onClick: self.tabClickHandlerFactory(id),
active: self.state.activeTabId === id
})
)
}))
}
if (child.type == TabPanel) {
const { id } = child.props
const active = self.state.activeTabId === id
return active && React.cloneElement(child, { active: true })
}
})}
</div>
)
}
}
Tabs.propTypes = {
defaultActive: React.PropTypes.string.isRequired,
}
const TabBar = (props) => <div className='mdl-tabs__tab-bar' {...props}>{props.children}</div>
const TabLink = ({ active, ...props }) => {
return (
<a className={`mdl-tabs__tab${active ? ' is-active' : ''}`} {...props}>{props.children}</a>
)
}
const TabPanel = ({ active, ...props }) => (
<div className={`mdl-tabs__panel${active ? ' is-active' : ''}`} {...props}>{props.children}</div>
)
const PageWithTabs = (props) => (
<Tabs defaultActive={'targaryens-panel'}>
<TabBar>
<TabLink href="#starks-panel">{'Starks'}</TabLink>
<TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
<TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
</TabBar>
<TabPanel id="starks-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Eddard</li>
<li>Catelyn</li>
<li>Robb</li>
<li>Sansa</li>
<li>Brandon</li>
<li>Arya</li>
<li>Rickon</li>
</ul>
</TabPanel>
<TabPanel id="lannisters-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Tywin</li>
<li>Cersei</li>
<li>Jamie</li>
<li>Tyrion</li>
</ul>
</TabPanel>
<TabPanel id="targaryens-panel">
<ul style={{ listStyleType: 'none' }}>
<li>Viserys</li>
<li>Daenerys</li>
</ul>
</TabPanel>
</Tabs>
)
ReactDOM.render(<PageWithTabs/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.brown-orange.min.css" />
<div id='app'></div>
Upvotes: 2