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.

6b50fbb7ade3c6b576b2c2b8ed4b0f7d?s=128

David Kopal

July 12, 2018
Tweet

Transcript

  1. 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(<FilteredList list={SWChars} />)
  2. 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 ? ( <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={() => this.toggleState()}>Toggle</button> </div> ) : (<div>You need to have more than one character.</div>)}}
  3. 9.
  4. 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 ? ( <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={() => this.toggleState()}>Toggle</button> </div> ) : (<div>You need to have more than one character.</div>)}}
  5. 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 ? ( <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={() => this.toggleState()}>Toggle</button> </div> ) : (<div>You need to have more than one character.</div>)}}
  6. 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 => <div> {/* render UI */} </div>) Render(<DisplayList list={SWChars} />)
  7. 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 => <div> {/* render UI */} </div>) Render(<DisplayList list={SWChars} />) reusable
  8. 18.

    @coding_lawyer const FilteredList = ({ list, side }) => {

    const filteredList = list.filter(char => char.side === side) return filteredList.map(char => <div> {/* render UI */} </div>) } Render(<FilteredList side="dark" list={SWChars} />)
  9. 19.

    @coding_lawyer const FilteredList = ({ list, side }) => {

    const filteredList = list.filter(char => char.side === side) return filteredList.map(char => <div> {/* render UI */} </div>) } Render(<FilteredList side="dark" list={SWChars} />) not reusable
  10. 21.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })

    => { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } const DisplayList = ({ list }) => list.map(char => <div> {/* render UI */} </div>) const FilteredList = withFilterProps(DisplayList) Render(<FilteredList side="dark" list={SWChars} />)
  11. 22.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })=>

    { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } const FilteredList = withFilterProps(DisplayList)
  12. 23.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })=>

    { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } const FilteredList = withFilterProps(DisplayList) Higher-order component
  13. 25.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })=>

    { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } const FilteredList = withFilterProps(DisplayList) Higher-order component
  14. 26.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })=>

    { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } const FilteredList = withFilterProps(DisplayList) Higher-order component
  15. 27.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })

    => { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } const DisplayList = ({ list }) => list.map(char => <div> {/* render UI */} </div>) const FilteredList = withFilterProps(DisplayList) reusable
  16. 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 ? ( <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={() => this.toggleState()}>Toggle</button> </div> ) : (<div>You need to have more than one character.</div>)}}
  17. 30.
  18. 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 ? ( <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={() => this.toggleState()}>Toggle</button> </div> ) : (<div>You need to have more than one character.</div>)}}
  19. 32.
  20. 33.
  21. 36.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })

    => { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } const DisplayList = ({ list }) => list.map(char => <div> {/* render UI */} </div>) const FilteredList = withFilterProps(DisplayList) reusable
  22. 37.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })

    => { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> }
  23. 38.

    @coding_lawyer const withFilterProps = BaseComponent => ({ list, side })

    => { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } not reusable
  24. 40.

    @coding_lawyer not reusable const withFilterProps = BaseComponent => ({ list,

    side }) => { const transformedProps = list.filter(char => char.side === side) return <BaseComponent list={transformedProps} /> } reusable const withTransformProps = transformFunc => BaseComponent => baseProps => { const transformedProps = transformFunc(baseProps) return <BaseComponent {...transformedProps} /> }
  25. 41.

    @coding_lawyer const withTransformProps = transformFunc => BaseComponent => baseProps =>

    { const transformedProps = transformFunc(baseProps) return <BaseComponent {...transformedProps} /> } const DisplayList = ({ list }) => list.map(char => <div> {/* render UI */} </div>) const FilteredList = withTransformProps( ({ list, side }) => ({ list: list.filter(char => char.side === side) }))(DisplayList) Render(<FilteredList side="dark" list={SWChars} />)
  26. 42.

    @coding_lawyer const withTransformProps = transformFunc => BaseComponent => baseProps =>

    { const transformedProps = transformFunc(baseProps) return <BaseComponent {...transformedProps} /> } const DisplayList = ({ list }) => list.map(char => <div> {/* render UI */} </div>) const FilteredList = withTransformProps( ({ list, side }) => ({ list: list.filter(char => char.side === side) }))(DisplayList) Render(<FilteredList side="dark" list={SWChars} />)
  27. 43.

    @coding_lawyer const AddedList = withTransformProps( ({ list, side }) =>

    ({ list: [ …list, { name: 'Han Solo', side: 'light' } ] }))(DisplayList)
  28. 44.

    @coding_lawyer const withTransformProps = transformFunc => BaseComponent => baseProps =>

    { const transformedProps = transformFunc(baseProps) return <BaseComponent {...transformedProps} /> } const DisplayList = ({ list }) => list.map(char => <div> {/* render UI */} </div>) const FilteredList = withTransformProps( ({ list, side }) => ({ list: list.filter(char => char.side === side) }))(DisplayList) Render(<FilteredList side="dark" list={SWChars} />)
  29. 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)
  30. 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)
  31. 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)
  32. 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)
  33. 52.

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

    => conditionFn(baseProps) ? ( <EitherComponent /> ) : ( <BaseComponent {...baseProps} />)
  34. 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
  35. 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
  36. 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
  37. 57.

    @coding_lawyer const withTransformProps = transformFunc => BaseComponent => baseProps =>

    { const transformedProps = transformFunc(baseProps) return <BaseComponent {...transformedProps} /> } const withCondition = (conditionFn, EitherComponent) => BaseComponent => baseProps => conditionFn(baseProps) ? ( <EitherComponent /> ) : ( <BaseComponent {...baseProps} /> ) const ConditionedList = withCondition(({ list }) => list.length <= 1, Warning)(DisplayList) const FilteredList = withTransformProps(({ list, side }) => ({ list: list.filter(char => char.side === side) }))(ConditionedList)
  38. 58.

    @coding_lawyer const DisplayList = ({ list }) => list.map(char =>

    <div> {/* render UI */} </div>) const Warning = () => <div> You need to have more than one character </div> Render(<FilteredList side="dark" list={SWChars} />)
  39. 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)
  40. 61.

    @coding_lawyer const FilteredList = withTransformProps(({ list, side }) => ({

    list: list.filter(char => char.side === side) }))(withCondition(({ list }) => list.length <= 1, Warning)(DisplayList))
  41. 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)
  42. 63.

    @coding_lawyer const withTransformProps = transformFunc => BaseComponent => baseProps =>

    { const transformedProps = transformFunc(baseProps) return <BaseComponent {...transformedProps} /> } const withCondition = (conditionFn, EitherComponent) => BaseComponent => baseProps => conditionFn(baseProps) ? ( <EitherComponent /> ) : ( <BaseComponent {...baseProps} /> ) const enhance = compose( withTransformProps(({ list, side }) => ({ list: list.filter(char => char.side === side) })), withCondition(({ list }) => list.length <= 1, Warning) ) const FilteredList = enhance(DisplayList)
  43. 65.
  44. 67.

    @coding_lawyer import { withProps, branch, compose } from 'recompose' const

    withWarning = () => () => <div>You need to have more than one character</div> const enhance = compose( withProps(({ list, side }) => ({ list: list.filter(char => char.side === side) })), branch(({ list }) => list.length <= 1, withWarning) ) const FilteredList = enhance(DisplayList)
  45. 68.

    @coding_lawyer import { withProps, branch, compose, withState } from ‘recompose'

    const withWarning = () => () => ( <div>You need to have more than one character</div> ) const enhance = compose( withState('state', 'setState', 'dark'), withProps(({ list, state }) => ({ filtered: list.filter(char => char.side === state) })), branch(({ filtered }) => filtered.length <= 1, withWarning) )
  46. 69.

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

    ( <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={() => setState(state === 'dark' ? 'light' : 'dark')}> Toggle </button> </div> ) const FilteredList = enhance(DisplayList) Render(<FilteredList list={SWChars} />)
  47. 70.

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

    ( <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={() => setState(state === 'dark' ? 'light' : 'dark')}> Toggle </button> </div> ) const FilteredList = enhance(DisplayList) Render(<FilteredList list={SWChars} />)
  48. 71.

    @coding_lawyer const withWarning = () => () => ( <div>You

    need to have more than one character</div> ) 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') }) )
  49. 72.

    @coding_lawyer const withWarning = () => () => ( <div>You

    need to have more than one character</div> ) 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') }) )
  50. 73.

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

    <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={toggleState}>Toggle</button> </div> ) const FilteredList = enhance(DisplayList) Render(<FilteredList list={SWChars} />)
  51. 74.

    @coding_lawyer const withWarning = () => () => ( <div>You

    need to have more than one character</div> ) 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') }) )
  52. 76.

    @coding_lawyer withState (logic) reusable reusable reusable withProps (logic) branch (logic)

    withHandlers (logic) DisplayList (presentation) reusable reusable
  53. 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 ? ( <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={() => this.toggleState()}>Toggle</button> </div> ) : (<div>You need to have more than one character.</div>)}}
  54. 79.

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

    <div> {filtered.map(char => ( <div key={char.name}> <div>Character: {char.name}</div> <div>Side: {char.side}</div> </div> ))} <button onClick={toggleState}>Toggle</button> </div> ) const FilteredList = enhance(DisplayList)
  55. 80.

    @coding_lawyer const withWarning = () => () => ( <div>You

    need to have more than one character</div> ) 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') }) )
  56. 83.

    @coding_lawyer const withWarning = () => () => ( <div>You

    need to have more than one character</div> ) 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') }) )
  57. 84.

    @coding_lawyer const withWarning = () => () => ( <div>You

    need to have more than one character</div> ) 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') }) )