- Give more rendering control to the user
- The functionality of the component stays intact, but the look and the order of the children can be changed
- Allow an implicit way of sharing state between components without manually passing it
const App = () => (
<Select defaultValue="ketchup">
{({activeValue, onSelect}) => (
<Fragment>
<Option onSelect={onSelect} value="ketchup" active={activeValue}>
Ketchup
</Option>
<Option onSelect={onSelect} value="mustard" active={activeValue}>
Mustard
</Option>
<Option onSelect={onSelect} value="mayonnaise" active={activeValue}>
Mayonnaise
</Option>
</Fragment>
)}
</Select>
);
- We can, but sometimes you want to hide all the magic, but still give some control to the user. See how simple it looks below compared as to the render prop version at the top.
const App = () => (
<Tabs defaultValue="home">
<Tab value="home"> Active </Tab>
<Tab value="about"> About </Tab>
<Tab value="contact"> Contact </Tab>
</Tabs>
);
But how does Tabs give any information to Tab without explicitly passing props? What about the nested children?
The underlying question can be resolved with the following.
- React.cloneElement()
- Context
- react-call-return
The Tab component
Dumb af, it doesn't care how they're pass and it just wants to receive props
const Tab = ({active, onSelect, children}) => (
<div
onClick={onSelect}
style={{
backgroundColor: active ? 'purple' : 'white'
}}
>
{children}
</div>
);
The Tabs Component
- should contain all the state
- Should pass the props to each tab component
class Tabs extends Component {
state = {
selectedValue: this.props.defaultValue
};
selectTab = value => {
this.setState({selectedValue: value});
};
render() {
const {children} = this.props; //all tabs
return magicallyPassPropsToChildren;
}
}
Tabs.js
class Tabs extends Component {
state = {
selectedValue: this.props.defaultValue
};
selectTab = value => {
this.setState({selectedValue: value});
};
render() {
const {children: tabs} = this.props; //all tabs
return React.Children.map(tabs, tab =>
React.cloneElement(tab, {
active: tab.props.value === this.state.selectedValue,
onSelect: () => this.selectTab(tab.props.value)
})
);
}
}
App.js
const MagicTabs = () => [
<Tab value="home"> Home </Tab>,
<Tab value="team"> Team </Tab>
];
const App = () => (
<Tabs defaultValue="home">
<div>
<Tab value="home"> Active </Tab>
<Tab value="about"> About </Tab>
</div>
<Tab value="contact"> Contact </Tab>
<Tab value="other"> Other </Tab>
<MagicTabs />
</Tabs>
);
The above implentation assumes every direct child of <Tabs />
in App.js
is <Tab />
. However that is not the case when developers start to tinker with Tabs
and start putting them in <div>
or make use of them in separate components such as MagicTabs
.
Soooo... context API to the rescue.
Remember to have React 16.3 or above. In the previous versions of React it was not recommended.
//create context
const ThemeContext = React.createContext('light');
This gives us two components:
- ThemeContext.Provider
- ThemeContext.Consumer
const Header = () => (
<div>
<ThemeContext.Consumer>
{theme => (
<div style={{backgroundColor: theme === 'dark' ? 'black' : 'white'}}>
Hello user
</div>
)}
</ThemeContext.Consumer>
</div>
);
class App extends Component {
state = {
theme: 'dark'
};
render() {
return (
<div>
<ThemeContext.Provider value={this.state.theme}>
<h1> Hello world </h1>
<Header />
</ThemeContext.Provider>
</div>
);
}
}
Well....
- If a component uses a Consumer but a Provider is not providing any value, the default value from .createContext(defaultValue) will be used
- i.e if the context is const ThemeContext = React.createContext('white') the default value will be white.
- The provided value (and the default value) can be of any type: array, object, string, number.
const TabsContext = React.createContext();
class Tabs extends React.Component {
state = {selectedValue: this.props.defaultValue};
selectTab = value => {
this.setState({selectedValue: value});
};
render() {
const {children} = this.props; //all tabs
return (
<TabsContext.Provider
value={{
selectTab: this.selectTab,
selectedValue: this.state.selectedValue
}}
>
{children}
</TabsContext.Provider>
);
}
}
It is possible to wrap the whole return value of render
with the context.
const Tab = ({value, children}) => (
<TabsContext.Consumer>
{({selectTab, selectedValue}) => (
<div
onClick={() => selectTab(value)}
style={{
backgroundColor: value === selectedValue ? 'purple' : 'white'
}}
>
{children}
</div>
)}
</TabsContext.Consumer>
);
However this can be repettive if context is not just for Tabs
. In that case write high order component (HOC)
withCotext.js
const withTabContext = Component => props => {
return (
<TabsContext.Consumer>
{({selectTab, selectedValue}) => {
return (
<Component
{...props}
active={props.value === selectedValue}
onSelect={() => selectTab(props.value)}
/>
);
}}
</TabsContext.Consumer>
);
};
Tabs.js
const Tab = () => <div> ... </div>;
const withTabContext = () => {};
class Tabs extends React.Component {
static Tab = withTabContext(Tab);
}
App.js
const App = () => (
<Tabs defaultValue="home">
<Tabs.Tab value="home"> Active </Tabs.Tab>
<Tabs.Tab value="about"> About </Tabs.Tab>
<Tabs.Tab value="contact"> Contact </Tabs.Tab>
</Tabs>
);
Finally, if this is the first time looking at this pattern, then all I can say it is powerful for separating component compostion from basic/dumb/atoms/stateless components and just rely on the compound (the parent component) to manage the props it needs. Therefore becomes easier to debug as the first intention is to look into the compound component.
It allows developers to look at a compound component and easily add to it, e.g. in this case adding a new prop without the need to change in other places.
And last of all achieves DRY and KISS. We are not repeating onSelected prop, and it takes 5 lines of code to render a tabs navigation when Tabs
is impoorted.