A presentation at ReactNext 2018 in November 2018 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