Beautiful Abstractions: What I Learned Reading the Source-Code of 18 React Libraries

A presentation at ReactNext 2018 in November 2018 in Tel Aviv-Yafo, Israel by Yonatan Mevorach

Slide 1

Slide 1

Slide 2

Slide 2

Lessons Learned Reading the Source-code of 18 React Libraries

Slide 3

Slide 3

Yonatan Mevorach @cowchimp

Slide 4

Slide 4

Slide 5

Slide 5

9,000

Slide 6

Slide 6

Slide 7

Slide 7

Slide 8

Slide 8

Slide 9

Slide 9

api implementation

Slide 10

Slide 10

💜

Slide 11

Slide 11

Slide 12

Slide 12

export default class OnlineIndicator extends Component { state = { isOnline: true //TODO: 🤔 } render() { return this.state.isOnline ? ( <OnlineIcon /> ) : ( <OfflineIcon /> ); } }

Slide 13

Slide 13

react-network

Slide 14

Slide 14

<Network render={({ online }) => {online ? ( <OnlineIcon /> ) : ( <OfflineIcon /> )} }/>

Slide 15

Slide 15

Slide 16

Slide 16

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) } }

Slide 17

Slide 17

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) } }

Slide 18

Slide 18

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) } }

Slide 19

Slide 19

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) } }

Slide 20

Slide 20

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) } }

Slide 21

Slide 21

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) } }

Slide 22

Slide 22

react-*

Slide 23

Slide 23

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) } }

Slide 24

Slide 24

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) } }

Slide 25

Slide 25

LESSON #1: Abstract away the Browser’s APIs by creating your own Declarative React Components

Slide 26

Slide 26

LESSON #2: Use Headless Components & the Render Props pattern to promote reusability & extensibility

Slide 27

Slide 27

LESSON #2 V2: Use Headless Components & the Render Props pattern to promote reusability & extensibility (& children-as-a-function & HOCs)

Slide 28

Slide 28

react-router

Slide 29

Slide 29

Slide 30

Slide 30

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) } }

Slide 31

Slide 31

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) } }

Slide 32

Slide 32

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)🤔 } }

Slide 33

Slide 33

<CheapRouter render={({ location }) => <CheapLink location={location} to="/" /> } />

Slide 34

Slide 34

<CheapRouter render={App} /> function App(props) { return ( <Products {...props} /> ) } function Products(props) { return ( <CheapLink location={props.location} to="/" /> ) }

Slide 35

Slide 35

function App() { return ( <CheapRouter> <Products /> </CheapRouter> ) } function Products() { return ( <CheapLink to="/" /> ) }

Slide 36

Slide 36

Context

Slide 37

Slide 37

// CheapRouterContext.js import { createContext } from 'react'; export default createContext();

Slide 38

Slide 38

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 ( ) } }

Slide 39

Slide 39

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 /> ) } }

Slide 40

Slide 40

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} /> ) } }

Slide 41

Slide 41

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>

Slide 42

Slide 42

import CheapRouterContext from './CheapRouterContext'; export default class CheapLink extends Component { render() { return ( ) } }

Slide 43

Slide 43

import CheapRouterContext from './CheapRouterContext'; export default class CheapLink extends Component { render() { return ( <CheapRouterContext.Consumer> { ({location}) => console.log(location) } </CheapRouterContext.Consumer> ) } }

Slide 44

Slide 44

LESSON #3: In cases where an entire component sub-tree needs the same data, expose it via Context

Slide 45

Slide 45

react-tracking

Slide 46

Slide 46

export default class FooPage extends React.Component { handleClick() { // ... other stuff } render() { return <a onClick={this.handleClick}>Click Me!</a>; } }

Slide 47

Slide 47

@track({ page: 'FooPage' }) export default class FooPage extends React.Component { handleClick() { // ... other stuff } render() { return <a onClick={this.handleClick}>Click Me!</a>; } }

Slide 48

Slide 48

@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>; } }

Slide 49

Slide 49

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 } )

Slide 50

Slide 50

export default function track(trackingInfo) { return function (...args) { console.log(args); } } [ instance, 'handleClick', { configurable: true, enumerable: false, writable: true, value: ƒ() } ] target name descriptor

Slide 51

Slide 51

export default function track(trackingInfo) { return function (target, name, descriptor) { } }

Slide 52

Slide 52

export default function track(trackingInfo) { return function (target, name, descriptor) { return { configurable: descriptor.configurable, enumerable: descriptor.enumerable, } } }

Slide 53

Slide 53

export default function track(trackingInfo) { return function (target, name, descriptor) { return { configurable: descriptor.configurable, enumerable: descriptor.enumerable, value: function(...args) { } } } }

Slide 54

Slide 54

export default function track(trackingInfo) { return function (target, name, descriptor) { return { configurable: descriptor.configurable, enumerable: descriptor.enumerable, value: function(...args) { trackEvent(trackingInfo); } } } }

Slide 55

Slide 55

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); } } } }

Slide 56

Slide 56

LESSON #4: Exposing Decorators lets your users integrate with your library with the minimal amount of boilerplate

Slide 57

Slide 57

Slide 58

Slide 58

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>

Slide 59

Slide 59

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>

Slide 60

Slide 60

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;; 🤔

Slide 61

Slide 61

styled.acolor: white; ${props => props.primary && color: red; } border: 2px solid white; styled.a = function(styles, ...interpolations) { }

Slide 62

Slide 62

styled.acolor: white; ${props => props.primary && color: red; } border: 2px solid white; styled.a = function(styles, ...interpolations) { console.log(styles); console.log(interpolations); }

Slide 63

Slide 63

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)]

Slide 64

Slide 64

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); }

Slide 65

Slide 65

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;']

Slide 66

Slide 66

<Anchor href="/getting-started" primary> Start </Anchor> [ 'color: white;', ƒ(props), 'border: 2px solid white;' ] {

  • primary: true } [ 'color: white;', 'color: red;' 'border: 2px solid white;' = ]

Slide 67

Slide 67

<Anchor href="/getting-started" primary> Start </Anchor> <Anchor href="/contact" primary> Contact Us </Anchor>

Slide 68

Slide 68

<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> 💩

Slide 69

Slide 69

<head> <style> .iUUGwG { color:white; color:red; border:2px solid white; } </style> </head> <a class="iUUGwG" href="/getting-started">Start</a> <a class="iUUGwG" href="/contact-us">Contact Us </a>

Slide 70

Slide 70

[ 'color: white;', 'color: red;' 'border: 2px solid white;' ] const name = hasher(css);

Slide 71

Slide 71

[ 'color: white;', 'color: red;' 'border: 2px solid white;' ] const name = hasher(css); if (!styleSheet.hasName(name)) { }

Slide 72

Slide 72

[ 'color: white;', 'color: red;' 'border: 2px solid white;' ] const name = hasher(css); if (!styleSheet.hasName(name)) { styleSheet.inject(css, '.' + name); } document.head.append

Slide 73

Slide 73

LESSON #5: Use Tagged Template Literals to make it easy to combine different DSLs with React Code

Slide 74

Slide 74

LESSON #6: You can escape out of the React rendering context, & render anywhere in the DOM

Slide 75

Slide 75

LESSON #6: You can escape out of the React rendering context, & render anywhere in the DOM (but be careful !!!)

Slide 76

Slide 76

cb http://flic.kr/p/yCzHwJ

Slide 77

Slide 77

Thank You @cowchimp blog.cowchimp.com