Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unleash the power of the higher-order components

Unleash the power of the higher-order components

How to avoid unnecessary duplications in your React app? You want to make sure that your components are maintainable and reusable. You can achieve this by writing your code in a functional way using the higher-order components (HoCs). I want to show you how to define smart HoCs and use them to enhance presentational components. You'll also learn how to compose several HoCs to get more complex logic. Sticking to this pattern, you'll end up with reusable components that are readable and easy to test since each component is responsible only for a single task.

David Kopal

July 12, 2018
Tweet

More Decks by David Kopal

Other Decks in Programming

Transcript

  1. @coding_lawyer
    Unleash the power of the
    higher-order components
    David Kopal

    View Slide

  2. @coding_lawyer

    @coding_lawyer
    codinglawyer.net

    View Slide

  3. @coding_lawyer
    lambdup.io

    View Slide

  4. @coding_lawyer
    Who programs in React?

    View Slide

  5. @coding_lawyer
    React
    HoCs
    Reusability
    Composition
    Recompose
    Introduction Conclusion

    View Slide

  6. @coding_lawyer
    React
    HoCs
    Reusability
    Composition
    Recompose
    Introduction Conclusion

    View Slide

  7. @coding_lawyer
    const SWChars = [
    { name: 'Luke', side: 'light' },
    { name: 'Darth Vader', side: 'dark' },
    { name: 'Obi-wan Kenobi', side: 'light' },
    { name: 'Palpatine', side: 'dark' }
    ]
    Render()

    View Slide

  8. @coding_lawyer
    class FilteredList extends React.Component {
    constructor(props) {
    super(props)
    this.state = { side: 'dark' }
    }
    toggleState() {
    this.setState({ side: this.state.side === 'dark' ? 'light' : 'dark' })
    }
    render() {
    const filtered = this.props.list.filter(
    char => char.side === this.state.side)
    return filtered.length > 1 ? (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}
    this.toggleState()}>Toggle

    ) : (You need to have more than one character.)}}

    View Slide

  9. View Slide

  10. @coding_lawyer
    class FilteredList extends React.Component {
    constructor(props) {
    super(props)
    this.state = { side: 'dark' }
    }
    toggleState() {
    this.setState({ side: this.state.side === 'dark' ? 'light' : 'dark' })
    }
    render() {
    const filtered = this.props.list.filter(
    char => char.side === this.state.side)
    return filtered.length > 1 ? (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}
    this.toggleState()}>Toggle

    ) : (You need to have more than one character.)}}

    View Slide

  11. @coding_lawyer
    FilteredList
    (logic + presentation)
    not reusable

    View Slide

  12. @coding_lawyer
    class FilteredList extends React.Component {
    constructor(props) {
    super(props)
    this.state = { side: 'dark' }
    }
    toggleState() {
    this.setState({ side: this.state.side === 'dark' ? 'light' : 'dark' })
    }
    render() {
    const filtered = this.props.list.filter(
    char => char.side === this.state.side)
    return filtered.length > 1 ? (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}
    this.toggleState()}>Toggle

    ) : (You need to have more than one character.)}}

    View Slide

  13. @coding_lawyer
    React
    HoCs
    Reusability
    Composition
    Recompose
    Introduction Conclusion

    View Slide

  14. @coding_lawyer
    smart / presentational component

    View Slide

  15. @coding_lawyer
    const SWChars = [
    { name: 'Luke', side: 'light' },
    { name: 'Darth Vader', side: 'dark' },
    { name: 'Obi-wan Kenobi', side: 'light' },
    { name: 'Palpatine', side: 'dark' }
    ]
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    Render()

    View Slide

  16. @coding_lawyer
    const SWChars = [
    { name: 'Luke', side: 'light' },
    { name: 'Darth Vader', side: 'dark' },
    { name: 'Obi-wan Kenobi', side: 'light' },
    { name: 'Palpatine', side: 'dark' }
    ]
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    Render()
    reusable

    View Slide

  17. @coding_lawyer
    DisplayList
    (presentation)
    reusable

    View Slide

  18. @coding_lawyer
    const FilteredList = ({ list, side }) => {
    const filteredList = list.filter(char =>
    char.side === side)
    return filteredList.map(char =>
    {/* render UI */} )
    }
    Render()

    View Slide

  19. @coding_lawyer
    const FilteredList = ({ list, side }) => {
    const filteredList = list.filter(char =>
    char.side === side)
    return filteredList.map(char =>
    {/* render UI */} )
    }
    Render()
    not reusable

    View Slide

  20. @coding_lawyer
    FilteredList
    (logic + presentation)
    not reusable

    View Slide

  21. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side }) => {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    const FilteredList = withFilterProps(DisplayList)
    Render()

    View Slide

  22. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side })=> {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    const FilteredList = withFilterProps(DisplayList)

    View Slide

  23. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side })=> {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    const FilteredList = withFilterProps(DisplayList)
    Higher-order component

    View Slide

  24. @coding_lawyer
    const HoC =
    BaseComponent =>
    EnhancedComponent

    View Slide

  25. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side })=> {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    const FilteredList = withFilterProps(DisplayList)
    Higher-order component

    View Slide

  26. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side })=> {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    const FilteredList = withFilterProps(DisplayList)
    Higher-order component

    View Slide

  27. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side }) => {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    const FilteredList = withFilterProps(DisplayList)
    reusable

    View Slide

  28. @coding_lawyer
    DisplayList
    (presentation)
    withFilterProps
    (logic)
    reusable

    View Slide

  29. @coding_lawyer
    class FilteredList extends React.Component {
    constructor(props) {
    super(props)
    this.state = { side: 'dark' }
    }
    toggleState() {
    this.setState({ side: this.state.side === 'dark' ? 'light' : 'dark' })
    }
    render() {
    const filtered = this.props.list.filter(
    char => char.side === this.state.side)
    return filtered.length > 1 ? (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}
    this.toggleState()}>Toggle

    ) : (You need to have more than one character.)}}

    View Slide

  30. View Slide

  31. @coding_lawyer
    class FilteredList extends React.Component {
    constructor(props) {
    super(props)
    this.state = { side: 'dark' }
    }
    toggleState() {
    this.setState({ side: this.state.side === 'dark' ? 'light' : 'dark' })
    }
    render() {
    const filtered = this.props.list.filter(
    char => char.side === this.state.side)
    return filtered.length > 1 ? (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}
    this.toggleState()}>Toggle

    ) : (You need to have more than one character.)}}

    View Slide

  32. View Slide

  33. View Slide

  34. @coding_lawyer
    React
    HoCs
    Reusability
    Composition
    Recompose
    Introduction Conclusion

    View Slide

  35. @coding_lawyer
    decoupling component from the data

    View Slide

  36. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side }) => {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    const FilteredList = withFilterProps(DisplayList)
    reusable

    View Slide

  37. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side }) => {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }

    View Slide

  38. @coding_lawyer
    const withFilterProps = BaseComponent =>
    ({ list, side }) => {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    not reusable

    View Slide

  39. @coding_lawyer
    DisplayList
    (presentation)
    withFilterProps
    (logic)
    not reusable
    reusable

    View Slide

  40. @coding_lawyer
    not reusable
    const withFilterProps = BaseComponent =>
    ({ list, side }) => {
    const transformedProps = list.filter(char =>
    char.side === side)
    return
    }
    reusable
    const withTransformProps = transformFunc =>
    BaseComponent => baseProps => {
    const transformedProps = transformFunc(baseProps)
    return
    }

    View Slide

  41. @coding_lawyer
    const withTransformProps = transformFunc =>
    BaseComponent => baseProps => {
    const transformedProps = transformFunc(baseProps)
    return
    }
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    const FilteredList = withTransformProps(
    ({ list, side }) => ({
    list: list.filter(char =>
    char.side === side)
    }))(DisplayList)
    Render()

    View Slide

  42. @coding_lawyer
    const withTransformProps = transformFunc =>
    BaseComponent => baseProps => {
    const transformedProps = transformFunc(baseProps)
    return
    }
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    const FilteredList = withTransformProps(
    ({ list, side }) => ({
    list: list.filter(char =>
    char.side === side)
    }))(DisplayList)
    Render()

    View Slide

  43. @coding_lawyer
    const AddedList = withTransformProps(
    ({ list, side }) => ({
    list: [
    …list,
    { name: 'Han Solo', side: 'light' }
    ]
    }))(DisplayList)

    View Slide

  44. @coding_lawyer
    const withTransformProps = transformFunc =>
    BaseComponent => baseProps => {
    const transformedProps = transformFunc(baseProps)
    return
    }
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    const FilteredList = withTransformProps(
    ({ list, side }) => ({
    list: list.filter(char =>
    char.side === side)
    }))(DisplayList)
    Render()

    View Slide

  45. @coding_lawyer
    const HoC = config =>
    BaseComponent =>
    EnhancedComponent

    View Slide

  46. @coding_lawyer
    avoid unnecessary duplication

    View Slide

  47. @coding_lawyer
    DisplayList
    (presentation)
    withTransformProps
    (logic)
    reusable
    reusable

    View Slide

  48. @coding_lawyer
    import { connect } from 'react-redux'
    const mapStateToProps = state => ({
    /* piece of global state */
    })
    const mapDispatchToProps = dispatch => ({
    /* global state handlers*/
    })
    export default connect(
    mapStateToProps,
    mapDispatchToProps
    )(Component)

    View Slide

  49. @coding_lawyer
    import { connect } from 'react-redux'
    const mapStateToProps = state => ({
    /* piece of global state */
    })
    const mapDispatchToProps = dispatch => ({
    /* global state handlers*/
    })
    export default connect(
    mapStateToProps,
    mapDispatchToProps
    )(Component)

    View Slide

  50. @coding_lawyer
    import { connect } from 'react-redux'
    const mapStateToProps = state => ({
    /* piece of global state */
    })
    const mapDispatchToProps = dispatch => ({
    /* global state handlers*/
    })
    export default connect(
    mapStateToProps,
    mapDispatchToProps
    )(Component)

    View Slide

  51. @coding_lawyer
    import { connect } from 'react-redux'
    const mapStateToProps = state => ({
    /* piece of global state */
    })
    const mapDispatchToProps = dispatch => ({
    /* global state handlers*/
    })
    export default connect(
    mapStateToProps,
    mapDispatchToProps
    )(Component)

    View Slide

  52. @coding_lawyer
    const withCondition = (conditionFn, EitherComponent) =>
    BaseComponent => baseProps =>
    conditionFn(baseProps) ? (

    ) : (
    )

    View Slide

  53. @coding_lawyer
    React
    HoCs
    Reusability
    Composition
    Recompose
    Introduction Conclusion

    View Slide

  54. @coding_lawyer
    const number = 15
    const increment = num => num + 5
    const decrement = num => num - 3
    const multiply = num => num * 2
    const operation = increment(decrement(multiply(number)))
    console.log(operation) //32

    View Slide

  55. @coding_lawyer
    const number = 15
    const increment = num => num + 5
    const decrement = num => num - 3
    const multiply = num => num * 2
    const operation = increment(decrement(multiply(number)))
    console.log(operation) //32

    View Slide

  56. @coding_lawyer
    const number = 15
    const increment = num => num + 5
    const decrement = num => num - 3
    const multiply = num => num * 2
    const operation = increment(decrement(multiply(number)))
    console.log(operation) //32

    View Slide

  57. @coding_lawyer
    const withTransformProps = transformFunc =>
    BaseComponent => baseProps => {
    const transformedProps = transformFunc(baseProps)
    return
    }
    const withCondition = (conditionFn, EitherComponent) =>
    BaseComponent => baseProps =>
    conditionFn(baseProps) ? (

    ) : (

    )
    const ConditionedList = withCondition(({ list }) =>
    list.length <= 1, Warning)(DisplayList)
    const FilteredList = withTransformProps(({ list, side }) => ({
    list: list.filter(char => char.side === side)
    }))(ConditionedList)

    View Slide

  58. @coding_lawyer
    const DisplayList = ({ list }) =>
    list.map(char => {/* render UI */} )
    const Warning = () =>

    You need to have more than one character

    Render()

    View Slide

  59. @coding_lawyer
    HoCs talk to each other via props

    View Slide

  60. @coding_lawyer
    const ConditionedList = withCondition(({ list }) =>
    list.length <= 1, Warning)(DisplayList)
    const FilteredList = withTransformProps(({ list, side }) => ({
    list: list.filter(char => char.side === side)
    }))(ConditionedList)

    View Slide

  61. @coding_lawyer
    const FilteredList = withTransformProps(({ list, side }) => ({
    list: list.filter(char => char.side === side)
    }))(withCondition(({ list }) =>
    list.length <= 1, Warning)(DisplayList))

    View Slide

  62. @coding_lawyer
    const compose = (...hocs) => BaseComponent =>
    hocs.reduceRight((acc, hoc) => hoc(acc), BaseComponent)
    const enhance = compose(
    withTransformProps(({ list, side }) => ({
    list: list.filter(char => char.side === side)
    })),
    withCondition(({ list }) => list.length <= 1, Warning)
    )
    const FilteredList = enhance(DisplayList)

    View Slide

  63. @coding_lawyer
    const withTransformProps = transformFunc =>
    BaseComponent => baseProps => {
    const transformedProps = transformFunc(baseProps)
    return
    }
    const withCondition = (conditionFn, EitherComponent) =>
    BaseComponent => baseProps =>
    conditionFn(baseProps) ? (

    ) : (

    )
    const enhance = compose(
    withTransformProps(({ list, side }) => ({
    list: list.filter(char => char.side === side)
    })),
    withCondition(({ list }) => list.length <= 1, Warning)
    )
    const FilteredList = enhance(DisplayList)

    View Slide

  64. @coding_lawyer
    withTransformProps
    (logic)
    reusable
    reusable
    withCondition
    (logic)
    DisplayList
    (presentation)
    reusable

    View Slide

  65. View Slide

  66. @coding_lawyer
    React
    HoCs
    Reusability
    Composition
    Recompose
    Introduction Conclusion

    View Slide

  67. @coding_lawyer
    import { withProps, branch, compose } from 'recompose'
    const withWarning = () => () =>
    You need to have more than one character
    const enhance = compose(
    withProps(({ list, side }) => ({
    list: list.filter(char => char.side === side)
    })),
    branch(({ list }) => list.length <= 1, withWarning)
    )
    const FilteredList = enhance(DisplayList)

    View Slide

  68. @coding_lawyer
    import { withProps, branch, compose, withState } from
    ‘recompose'
    const withWarning = () => () => (
    You need to have more than one character
    )
    const enhance = compose(
    withState('state', 'setState', 'dark'),
    withProps(({ list, state }) => ({
    filtered: list.filter(char => char.side === state)
    })),
    branch(({ filtered }) => filtered.length <= 1, withWarning)
    )

    View Slide

  69. @coding_lawyer
    const DisplayList = ({ filtered, setState, state }) => (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}

    setState(state === 'dark' ? 'light' : 'dark')}>
    Toggle


    )
    const FilteredList = enhance(DisplayList)
    Render()

    View Slide

  70. @coding_lawyer
    const DisplayList = ({ filtered, setState, state }) => (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}

    setState(state === 'dark' ? 'light' : 'dark')}>
    Toggle


    )
    const FilteredList = enhance(DisplayList)
    Render()

    View Slide

  71. @coding_lawyer
    const withWarning = () => () => (
    You need to have more than one character
    )
    const enhance = compose(
    withState('state', 'setState', 'dark'),
    withProps(({ list, state }) => ({
    filtered: list.filter(char => char.side === state)
    })),
    branch(({ filtered }) => filtered.length <= 1, withWarning),
    withHandlers({
    toggleState: ({ state, setState }) => () =>
    setState(state === 'dark' ? 'light' : 'dark')
    })
    )

    View Slide

  72. @coding_lawyer
    const withWarning = () => () => (
    You need to have more than one character
    )
    const enhance = compose(
    withState('state', 'setState', 'dark'),
    withProps(({ list, state }) => ({
    filtered: list.filter(char => char.side === state)
    })),
    branch(({ filtered }) => filtered.length <= 1, withWarning),
    withHandlers({
    toggleState: ({ state, setState }) => () =>
    setState(state === 'dark' ? 'light' : 'dark')
    })
    )

    View Slide

  73. @coding_lawyer
    const DisplayList = ({ filtered, toggleState }) => (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}
    Toggle

    )
    const FilteredList = enhance(DisplayList)
    Render()

    View Slide

  74. @coding_lawyer
    const withWarning = () => () => (
    You need to have more than one character
    )
    const enhance = compose(
    withState('state', 'setState', 'dark'),
    withProps(({ list, state }) => ({
    filtered: list.filter(char => char.side === state)
    })),
    branch(({ filtered }) => filtered.length <= 1, withWarning),
    withHandlers({
    toggleState: ({ state, setState }) => () =>
    setState(state === 'dark' ? 'light' : 'dark')
    })
    )

    View Slide

  75. @coding_lawyer
    Recompose makes HoCs’ composition
    easier

    View Slide

  76. @coding_lawyer
    withState
    (logic)
    reusable
    reusable
    reusable
    withProps
    (logic)
    branch
    (logic)
    withHandlers
    (logic)
    DisplayList
    (presentation)
    reusable
    reusable

    View Slide

  77. @coding_lawyer
    React
    HoCs
    Reusability
    Composition
    Recompose
    Introduction Conclusion

    View Slide

  78. @coding_lawyer
    class FilteredList extends React.Component {
    constructor(props) {
    super(props)
    this.state = { side: 'dark' }
    }
    toggleState() {
    this.setState({ side: this.state.side === 'dark' ? 'light' : 'dark' })
    }
    render() {
    const filtered = this.props.list.filter(
    char => char.side === this.state.side)
    return filtered.length > 1 ? (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}
    this.toggleState()}>Toggle

    ) : (You need to have more than one character.)}}

    View Slide

  79. @coding_lawyer
    const DisplayList = ({ filtered, toggleState }) => (

    {filtered.map(char => (

    Character: {char.name}
    Side: {char.side}

    ))}
    Toggle

    )
    const FilteredList = enhance(DisplayList)

    View Slide

  80. @coding_lawyer
    const withWarning = () => () => (
    You need to have more than one character
    )
    const enhance = compose(
    withState('state', 'setState', 'dark'),
    withProps(({ list, state }) => ({
    filtered: list.filter(char => char.side === state)
    })),
    branch(({ filtered }) => filtered.length <= 1, withWarning),
    withHandlers({
    toggleState: ({ state, setState }) => () =>
    setState(state === 'dark' ? 'light' : 'dark')
    })
    )

    View Slide

  81. @coding_lawyer
    presentational components that can be
    enhanced by HoC(s)

    View Slide

  82. @coding_lawyer
    data fetching
    local / global state
    local storage / cookies

    View Slide

  83. @coding_lawyer
    const withWarning = () => () => (
    You need to have more than one character
    )
    const enhance = compose(
    withState('state', 'setState', 'dark'),
    withProps(({ list, state }) => ({
    filtered: list.filter(char => char.side === state)
    })),
    branch(({ filtered }) => filtered.length <= 1, withWarning),
    withHandlers({
    toggleState: ({ state, setState }) => () =>
    setState(state === 'dark' ? 'light' : 'dark')
    })
    )

    View Slide

  84. @coding_lawyer
    const withWarning = () => () => (
    You need to have more than one character
    )
    const enhance = compose(
    withState('state', 'setState', 'dark'),
    withProps(({ list, state }) => ({
    filtered: list.filter(char => char.side === state)
    })),
    branch(({ filtered }) => filtered.length <= 1, withWarning),
    withHandlers({
    toggleState: ({ state, setState }) => () =>
    setState(state === 'dark' ? 'light' : 'dark')
    })
    )

    View Slide

  85. @coding_lawyer

    @coding_lawyer
    codinglawyer.net

    View Slide

  86. @coding_lawyer
    Questions?

    View Slide

  87. @coding_lawyer
    Thank You

    View Slide

  88. @coding_lawyer
    @coding_lawyer
    codinglawyer.net
    github.com/codinglawyer/hocs-code

    View Slide