A presentation at ReactNext 2018 in in Tel Aviv-Yafo, Israel by Yonatan Mevorach
Lessons Learned Reading the Source-code of 18 React Libraries
Yonatan Mevorach @cowchimp
9,000
api implementation
💜
export default class OnlineIndicator extends Component { state = { isOnline: true //TODO: 🤔 } render() { return this.state.isOnline ? ( <OnlineIcon /> ) : ( <OfflineIcon /> ); } }
react-network
<Network render={({ online }) => {online ? ( <OnlineIcon /> ) : ( <OfflineIcon /> )} }/>
export default class Network extends Component { static defaultProps = { render: () => null, onChange: () => {} } state = { online: window.navigator.onLine } componentDidMount() { window.addEventListener("offline", this.onChange) window.addEventListener("online", this.onChange) this.props.onChange(this.state) } componentWillUnmount() { window.removeEventListener("offline", this.onChange) window.removeEventListener("online", this.onChange) } onChange = () => { const online = window.navigator.onLine this.props.onChange({ online }) this.setState({ online }) } render() { return this.props.render(this.state) } }
export default class Network extends Component { static defaultProps = { render: () => null, onChange: () => {} } state = { online: window.navigator.onLine } componentDidMount() { window.addEventListener("offline", this.onChange) window.addEventListener("online", this.onChange) this.props.onChange(this.state) } componentWillUnmount() { window.removeEventListener("offline", this.onChange) window.removeEventListener("online", this.onChange) } onChange = () => { const online = window.navigator.onLine this.props.onChange({ online }) this.setState({ online }) } render() { return this.props.render(this.state) } }
export default class Network extends Component { static defaultProps = { render: () => null, onChange: () => {} } state = { online: window.navigator.onLine } componentDidMount() { window.addEventListener("offline", this.onChange) window.addEventListener("online", this.onChange) this.props.onChange(this.state) } componentWillUnmount() { window.removeEventListener("offline", this.onChange) window.removeEventListener("online", this.onChange) } onChange = () => { const online = window.navigator.onLine this.props.onChange({ online }) this.setState({ online }) } <Network render={ ({ online }) => { /.../ } }/> render() { return this.props.render(this.state) } }
export default class Network extends Component { static defaultProps = { render: () => null, onChange: () => {} } state = { online: window.navigator.onLine } componentDidMount() { window.addEventListener("offline", this.onChange) window.addEventListener("online", this.onChange) this.props.onChange(this.state) } componentWillUnmount() { window.removeEventListener("offline", this.onChange) window.removeEventListener("online", this.onChange) } onChange = () => { const online = window.navigator.onLine this.props.onChange({ online }) this.setState({ online }) } render() { return this.props.render(this.state) } }
export default class Network extends Component { static defaultProps = { render: () => null, onChange: () => {} } state = { online: window.navigator.onLine } componentDidMount() { window.addEventListener("offline", this.onChange) window.addEventListener("online", this.onChange) this.props.onChange(this.state) } componentWillUnmount() { window.removeEventListener("offline", this.onChange) window.removeEventListener("online", this.onChange) } onChange = () => { const online = window.navigator.onLine this.props.onChange({ online }) this.setState({ online }) } render() { return this.props.render(this.state) } }
export default class Network extends Component { static defaultProps = { render: () => null, onChange: () => {} } state = { online: window.navigator.onLine } componentDidMount() { window.addEventListener("offline", this.onChange) window.addEventListener("online", this.onChange) this.props.onChange(this.state) } componentWillUnmount() { window.removeEventListener("offline", this.onChange) window.removeEventListener("online", this.onChange) } onChange = () => { const online = window.navigator.onLine this.props.onChange({ online }) this.setState({ online }) } render() { return this.props.render(this.state) } }
react-*
export default class CheapMediaQuery extends Component { static defaultProps = { render: () => null } state = { screenWidth: window.screen.width } componentDidMount() { window.addEventListener("resize", this.onChange) } componentWillUnmount() { window.removeEventListener("resize", this.onChange) } onChange = () => { this.setState({ screenWidth: window.screen.width }) } render() { return this.props.render(this.state) } }
export default class CheapClock extends Component { static defaultProps = { render: () => null } state = { datetime: new Date() } componentDidMount() { this.intervalId = setInterval(this.onChange, 1000) } componentWillUnmount() { clearInterval(this.intervalId) } onChange = () => { this.setState({ datetime: new Date() }) } render() { return this.props.render(this.state) } }
LESSON #1: Abstract away the Browser’s APIs by creating your own Declarative React Components
LESSON #2: Use Headless Components & the Render Props pattern to promote reusability & extensibility
LESSON #2 V2: Use Headless Components & the Render Props pattern to promote reusability & extensibility (& children-as-a-function & HOCs)
react-router
export default class CheapRouter extends Component { static defaultProps = { render: () => null } state = { location: window.location.pathname } componentDidMount() { window.addEventListener("popstate", this.onChange) } componentWillUnmount() { window.removeEventListener("popstate", this.onChange) } onChange = () => { this.setState({ location: window.location.pathname }) } render() { return this.props.render(this.state) } }
export default class CheapRouter extends Component { static defaultProps = { render: () => null } state = { location: history.location } componentDidMount() { this.unlisten = history.listen(this.onChange) } componentWillUnmount() { this.unlisten() } onChange = (location) => { this.setState({ location: location }) } render() { return this.props.render(this.state) } }
export default class CheapRouter extends Component { static defaultProps = { render: () => null } state = { location: history.location } componentDidMount() { this.unlisten = history.listen(this.onChange) } componentWillUnmount() { this.unlisten() } onChange = (location) => { this.setState({ location: location }) } render() { return this.props.render(this.state)🤔 } }
<CheapRouter render={({ location }) => <CheapLink location={location} to="/" /> } />
<CheapRouter render={App} /> function App(props) { return ( <Products {...props} /> ) } function Products(props) { return ( <CheapLink location={props.location} to="/" /> ) }
function App() { return ( <CheapRouter> <Products /> </CheapRouter> ) } function Products() { return ( <CheapLink to="/" /> ) }
Context
// CheapRouterContext.js import { createContext } from 'react'; export default createContext();
import CheapRouterContext from './CheapRouterContext'; export default class CheapRouter extends Component { static defaultProps = { render: () => null } state = { location: history.location } componentDidMount() { this.unlisten = history.listen(this.onChange) } componentWillUnmount() { this.unlisten() } onChange = (location) => { this.setState({ location: location }) } render() { return ( ) } }
import CheapRouterContext from './CheapRouterContext'; export default class CheapRouter extends Component { static defaultProps = { render: () => null } state = { location: history.location } componentDidMount() { this.unlisten = history.listen(this.onChange) } componentWillUnmount() { this.unlisten() } onChange = (location) => { this.setState({ location: location }) } render() { return ( <CheapRouterContext.Provider /> ) } }
import CheapRouterContext from './CheapRouterContext'; export default class CheapRouter extends Component { static defaultProps = { render: () => null } state = { location: history.location } componentDidMount() { this.unlisten = history.listen(this.onChange) } componentWillUnmount() { this.unlisten() } onChange = (location) => { this.setState({ location: location }) } render() { return ( <CheapRouterContext.Provider value={this.state} /> ) } }
import CheapRouterContext from './CheapRouterContext'; export default class CheapRouter extends Component { static defaultProps = { render: () => null } state = { location: history.location } componentDidMount() { this.unlisten = history.listen(this.onChange) } componentWillUnmount() { this.unlisten() } onChange = (location) => { this.setState({ location: location }) } render() { return ( <CheapRouterContext.Provider value={this.state} children={this.props.children} /> ) } } <CheapRouter> <Products /> </CheapRouter>
import CheapRouterContext from './CheapRouterContext'; export default class CheapLink extends Component { render() { return ( ) } }
import CheapRouterContext from './CheapRouterContext'; export default class CheapLink extends Component { render() { return ( <CheapRouterContext.Consumer> { ({location}) => console.log(location) } </CheapRouterContext.Consumer> ) } }
LESSON #3: In cases where an entire component sub-tree needs the same data, expose it via Context
react-tracking
export default class FooPage extends React.Component { handleClick() { // ... other stuff } render() { return <a onClick={this.handleClick}>Click Me!</a>; } }
@track({ page: 'FooPage' }) export default class FooPage extends React.Component { handleClick() { // ... other stuff } render() { return <a onClick={this.handleClick}>Click Me!</a>; } }
@track({ page: 'FooPage' }) export default class FooPage extends React.Component { @track({ action: 'click' }) handleClick() { // ... other stuff } render() { return <a onClick={this.handleClick}>Click Me!</a>; } }
const calculator = { add: (n1, n2) => n1+n2 } = const calculator = {} Object.defineProperty( calculator, target 'add', { name configurable: true, enumerable: true, descriptor writable: true, value: (n1, n2) => n1+n2 } )
export default function track(trackingInfo) { return function (...args) { console.log(args); } } [ instance, 'handleClick', { configurable: true, enumerable: false, writable: true, value: Æ’() } ] target name descriptor
export default function track(trackingInfo) { return function (target, name, descriptor) { } }
export default function track(trackingInfo) { return function (target, name, descriptor) { return { configurable: descriptor.configurable, enumerable: descriptor.enumerable, } } }
export default function track(trackingInfo) { return function (target, name, descriptor) { return { configurable: descriptor.configurable, enumerable: descriptor.enumerable, value: function(...args) { } } } }
export default function track(trackingInfo) { return function (target, name, descriptor) { return { configurable: descriptor.configurable, enumerable: descriptor.enumerable, value: function(...args) { trackEvent(trackingInfo); } } } }
export default function track(trackingInfo) { return function (target, name, descriptor) { return { configurable: descriptor.configurable, enumerable: descriptor.enumerable, value: function(...args) { trackEvent(trackingInfo); descriptor.value.apply(this, args); } } } }
LESSON #4: Exposing Decorators lets your users integrate with your library with the minimal amount of boilerplate
const Anchor = styled.acolor: white; ${props => props.primary &&
color: red; } border: 2px solid white;
; <Anchor href="/getting-started" primary> Start </Anchor> <Anchor href="/docs"> Documentation </Anchor>
const Anchor = styled.acolor: white; ${props => props.primary &&
color: red; } border: 2px solid white;
;
<Anchor href="/getting-started" primary> Start </Anchor> <Anchor href="/docs"> Documentation </Anchor>
const Anchor = styled.a(color: white; ${props => props.primary &&
color: red; } border: 2px solid white;
);
' color: white; ${props => props.primary && color: red;
} border: 2px solid white; '
VS const Anchor = styled.acolor: white; ${props => props.primary &&
color: red; } border: 2px solid white;
;
🤔
styled.acolor: white; ${props => props.primary &&
color: red; } border: 2px solid white;
styled.a = function(styles, ...interpolations) { }
styled.acolor: white; ${props => props.primary &&
color: red; } border: 2px solid white;
styled.a = function(styles, ...interpolations) { console.log(styles); console.log(interpolations); }
styled.acolor: white; ${props => props.primary &&
color: red; } border: 2px solid white;
styled.a = function(styles, ...interpolations) { console.log(styles); console.log(interpolations); } ['color: white;', 'border: 2px solid white;'] [Æ’(props)]
styled.acolor: white; ${props => props.primary &&
color: red; } border: 2px solid white;
styled.a = function(styles, ...interpolations) { const rules = interleave(styles, interpolations) console.log(rules); }
styled.acolor: white; ${props => props.primary &&
color: red; } border: 2px solid white;
styled.a = function(styles, ...interpolations) { const rules = interleave(styles, interpolations) console.log(rules); } ['color: white;', Æ’(props), 'border: 2px solid white;']
<Anchor href="/getting-started" primary> Start </Anchor> [ 'color: white;', Æ’(props), 'border: 2px solid white;' ] {
<Anchor href="/getting-started" primary> Start </Anchor> <Anchor href="/contact" primary> Contact Us </Anchor>
<a style="color:white; color:red; border:2px solid white;" href="/getting-started">Start</a> <a style="color:white; color:red; border:2px solid white;" href="/contact-us">Contact Us</a> 💩
[ 'color: white;', 'color: red;' 'border: 2px solid white;' ] const name = hasher(css);
[ 'color: white;', 'color: red;' 'border: 2px solid white;' ] const name = hasher(css); if (!styleSheet.hasName(name)) { }
[ 'color: white;', 'color: red;' 'border: 2px solid white;' ] const name = hasher(css); if (!styleSheet.hasName(name)) { styleSheet.inject(css, '.' + name); } document.head.append
LESSON #5: Use Tagged Template Literals to make it easy to combine different DSLs with React Code
LESSON #6: You can escape out of the React rendering context, & render anywhere in the DOM
LESSON #6: You can escape out of the React rendering context, & render anywhere in the DOM (but be careful !!!)
cb http://flic.kr/p/yCzHwJ
Thank You @cowchimp blog.cowchimp.com
The open-source React community has produced some amazing 3rd-party libraries that make React development as powerful as it is. But what does it take to create a "beautiful abstraction", a library that hides a lot of complexity into a simple API? To answer this, I took a journey into the source code of 18 different React libraries, including of course the "big ones" like Redux, React Router, etc.
In this talk we'll examine the patterns that these libraries share, and how you can apply them your own React Components