Save 37% off PRO during our Black Friday Sale! »

Повторное использование кода с помощью HOC в React

Повторное использование кода с помощью HOC в React

РИТ++ 2018

F5c2731f9a4dbfb4af319295a1f0cd28?s=128

Dmitry Tsepelev

May 28, 2018
Tweet

Transcript

  1. Повторное использование кода с помощью HOC в React Дмитрий Цепелев

  2. Почему компоненты распухают? •  перерисовка •  граничные условия •  загрузка

    данных •  подготовка props •  side-эффекты 2
  3. Простое e-commerce приложение 3 ProductListContainer – компонент, отвечающий за загрузку

    списка товаров
  4. Простое e-commerce приложение 4 ProductList – компонент, отвечающий за отрисовку

    списка товаров
  5. Простое e-commerce приложение 5 ProductRow – компонент, отвечающий за отрисовку

    строки таблицы списка товаров
  6. Простое e-commerce приложение 6 AddToCartButton – компонент, отвечающий за отрисовку

    кнопки для добавления товара в корзину
  7. Простое e-commerce приложение 7 NavbarItem – компонент, отвечающий за отрисовку

    элемента навигационного меню
  8. Перерисовка компонентов 8 const ProductRow = ({ id, name, price

    }) => ( <tr> <td>{name}</td> <td>{price}</td> { } <td> <Link to={`/products/${id}`}>Details</Link> </td> </tr> )
  9. Перерисовка компонентов •  pure •  проверяет shallowEqual •  может работать

    медленно, если state и props объемные •  ложно-положительные срабатывания •  shouldComponentUpdate •  реализуем сами •  дает больше контроля 9
  10. Перерисовка компонентов 10 class ProductRow extends Component { shouldComponentUpdate(nextProps) {

    return nextProps.id !== this.props.id } render() { //... } }
  11. Принцип разделения ответственности •  Эдсгер Дейкстра, “On the role of

    scientific thought”, 1974 •  Крис Рид, «Элементы функционального программирования», 1989 •  программа разделяется на секции, ответственные за определенные области бизнес-логики 11
  12. Компонент высшего порядка (HOC) 12 const Name = () =>

    <p>Guest</p> const withGreeting = WrappedComponent => props => ( <div> Hi, <WrappedComponent {...props} /> </div> ) withGreeting(Name)
  13. Компонент высшего порядка (HOC) 13 const Name = () =>

    <p>Guest</p> const withGreeting = WrappedComponent => props => ( <div> Hi, <WrappedComponent {...props} /> </div> ) withGreeting(Name)
  14. Компонент высшего порядка (HOC) 14 const Name = () =>

    <p>Guest</p> const withGreeting = WrappedComponent => props => ( <div> Hi, <WrappedComponent {...props} /> </div> ) withGreeting(Name)
  15. Компонент высшего порядка (HOC) 15 const Name = () =>

    <p>Guest</p> const withGreeting = WrappedComponent => props => ( <div> Hi, <WrappedComponent {...props} /> </div> ) withGreeting(Name)
  16. onlyUpdateForKeys 16 const ProductRow = onlyUpdateForKeys(['id'])( ({ id, name, price

    /* ...больше свойств */ }) => ( <tr> <td>{name}</td> <td>{price}</td> <td> <Link to={`/products/${id}`}>Details</Link> </td> </tr> ) )
  17. onlyUpdateForKeys 17 const enhance = onlyUpdateForKeys(['id']) const ProductRow = ({

    id, name, price /* ...больше свойств */ }) => ( <tr> <td>{name}</td> <td>{price}</td> <td> <Link to={`/products/${id}`}>Details</Link> </td> </tr> ) export default enhance(ProductRow)
  18. onlyUpdateForKeys 18 const onlyUpdateForKeys = propKeys => WrappedComponent => class

    extends Component { shouldComponentUpdate(nextProps) { return propKeys.some(key => this.props[key] !== nextProps[key] ) } render() { return <WrappedComponent {...this.props} /> } }
  19. onlyUpdateForKeys 19 const onlyUpdateForKeys = propKeys => WrappedComponent => class

    extends Component { shouldComponentUpdate(nextProps) { return propKeys.some(key => this.props[key] !== nextProps[key] ) } render() { return <WrappedComponent {...this.props} /> } }
  20. onlyUpdateForKeys 20 const onlyUpdateForKeys = propKeys => WrappedComponent => class

    extends Component { shouldComponentUpdate(nextProps) { return propKeys.some(key => this.props[key] !== nextProps[key] ) } render() { return <WrappedComponent {...this.props} /> } }
  21. onlyUpdateForKeys 21 const onlyUpdateForKeys = propKeys => WrappedComponent => class

    extends Component { shouldComponentUpdate(nextProps) { return propKeys.some(key => this.props[key] !== nextProps[key] ) } render() { return <WrappedComponent {...this.props} /> } }
  22. onlyUpdateForKeys 22 const onlyUpdateForKeys = propKeys => WrappedComponent => class

    extends Component { shouldComponentUpdate(nextProps) { return propKeys.some(key => this.props[key] !== nextProps[key] ) } render() { return <WrappedComponent {...this.props} /> } }
  23. Простое e-commerce приложение 23 ProductList – компонент, отвечающий за отрисовку

    списка товаров
  24. Граничные условия в render 24 const ProductList = ({ products

    }) => ( <table> <thead> <tr> <th>Name</th> <th>Price</th> </tr> </thead> <tbody> {products.map(p => <ProductRow key={p.id} {...p} />)} </tbody> </table> )
  25. Граничные условия в render 25 const ProductList = ({ products

    }) => { if (!products.length) { return <NoData /> } return ( <table> {/* та же таблица */} </table> ) }
  26. Граничные условия в render 26 const ProductList = ({ loading,

    products }) => { if (loading) { return <Spinner /> } if (!products.length) { return <NoData /> } return (...) }
  27. Граничные условия в render 27 const ProductList = ({ currentUser,

    loading, products }) => { if (!currentUser) { return <NotAuthorized /> } if (loading) { return <Spinner /> } if (!products.length) { return <NoData /> } return (...) }
  28. 28 const enhance = branch( ({ products }) => !products.length,

    () => null ) const ProductList = ({ products }) => (...) export default enhance(ProductList) Граничные условия в render
  29. 29 const enhance = branch( ({ products }) => !products.length,

    () => null ) const ProductList = ({ products }) => (...) export default enhance(ProductList) Граничные условия в render
  30. 30 const enhance = branch( ({ products }) => !products.length,

    () => null ) const ProductList = ({ products }) => (...) export default enhance(ProductList) Граничные условия в render
  31. 31 const renderNothing = () => null const enhance =

    branch( ({ products }) => !products.length, renderNothing ) const ProductList = ({ products }) => (...) export default enhance(ProductList) Граничные условия в render
  32. branch 32 const branch = (cond, cb) => WrappedComponent =>

    props => cond(props) ? cb(props) : <WrappedComponent {...props} />
  33. branch 33 const branch = (cond, cb) => WrappedComponent =>

    props => cond(props) ? cb(props) : <WrappedComponent {...props} />
  34. Простое e-commerce приложение 34 ProductRow – компонент, отвечающий за отрисовку

    строки таблицы списка товаров
  35. Подготовка данных для отрисовки 35 const formattedPrice = price =>

    `$${price}` const enhance = onlyUpdateForKeys(['id']) const ProductRow = ({ id, name, price }) => ( <tr> <td>{name}</td> <td>{formattedPrice(price)}</td> <td> <Link to={`/products/${id}`}>Details</Link> </td> </tr> ) export default enhance(ProductRow)
  36. 36 const formattedPrice = price => `$${price}` const enhance =

    onlyUpdateForKeys(['id'])( withProps(({ price }) => ({ price: formattedPrice(price) })) ) const ProductRow = ({ id, name, price }) => (...) export default enhance(ProductRow) Подготовка данных для отрисовки
  37. withProps 37 const withProps = mapper => WrappedComponent => props

    => { const newProps = { ...props, ...mapper(props) } return <WrappedComponent {...newProps} /> }
  38. withProps 38 const withProps = mapper => WrappedComponent => props

    => { const newProps = { ...props, ...mapper(props) } return <WrappedComponent {...newProps} /> }
  39. Композиция 39 withRouter( injectIntl( connect(mapStateToProps, mapDispatchToProps) ) )(LandingPage)

  40. Композиция 40 const enhance = compose( withRouter, injectIntl, connect(mapStateToProps, mapDispatchToProps)

    ) enhance(LandingPage)
  41. compose 41 const compose = (...hocs) => WrappedComponent => hocs.reduce((acc,

    hoc) => hoc(acc), WrappedComponent)
  42. compose 42 const compose = (...hocs) => WrappedComponent => hocs.reduce((acc,

    hoc) => hoc(acc), WrappedComponent)
  43. compose 43 const compose = (...hocs) => WrappedComponent => hocs.reduce((acc,

    hoc) => hoc(acc), WrappedComponent)
  44. 44 const formattedPrice = price => `$${price}` const enhance =

    onlyUpdateForKeys(['id'])( withProps(({ price }) => ({ price: formattedPrice(price) })) ) const ProductRow = ({ id, name, price }) => (...) export default enhance(ProductRow) compose
  45. 45 const formattedPrice = price => `$${price}` const enhance =

    compose( onlyUpdateForKeys(['id']), withProps(({ price }) => ({ price: formattedPrice(price) })) ) const ProductRow = ({ id, name, price }) => (...) export default enhance(ProductRow) compose
  46. 46 const ProductList = ({ currentUser, loading, products }) =>

    { if (!currentUser) { return <NotAuthorized /> } if (loading) { return <Spinner /> } if (!products.length) { return <NoData /> } return (...) } Еще раз про граничные условия
  47. 47 const renderComponent = component => component const enhance =

    compose( branch( ({ currentUser }) => !currentUser, renderComponent(NotAuthorized) ), branch( ({ loading }) => loading, renderComponent(Spinner) ), branch( ({ products }) => !products.length, renderComponent(NoData) ) ) Еще раз про граничные условия
  48. const renderComponent = component => component const enhance = compose(

    branch( ({ currentUser }) => !currentUser, renderComponent(NotAuthorized) ), branch( ({ loading }) => loading, renderComponent(Spinner) ), branch( ({ products }) => !products.length, renderComponent(NoData) ) ) 48 Еще раз про граничные условия
  49. 49 const mapStateToProps = state => ({ currentUser: getCurrentUser(state) })

    const enhance = compose( connect(mapStateToProps), branch( ({ currentUser }) => !currentUser, renderComponent(NotAuthorized) ), ... ) Еще раз про граничные условия
  50. 50 const checkAuthorization = compose( connect(state => ({ currentUser: getCurrentUser(state)

    })), branch( ({ currentUser }) => !currentUser, renderComponent(NotAuthorized) ) ) Еще раз про граничные условия
  51. 51 const enhance = compose( checkAuthorization, branch( ({ loading })

    => loading, renderComponent(Spinner) ), branch(({ products }) => !products.length, renderComponent(NoData)) ) const ProductList = ({ products }) => (...) export default enhance(ProductList) Еще раз про граничные условия
  52. 52 const enhance = compose( checkAuthorization, branch( ({ loading })

    => loading, renderComponent(Spinner) ), branch(({ products }) => !products.length, renderComponent(NoData)) ) const ProductList = ({ products }) => (...) export default enhance(ProductList) Еще раз про граничные условия
  53. 53 const showSpinnerWhileLoading = branch( ({ loading }) => loading,

    renderComponent(Spinner) ) Еще раз про граничные условия
  54. 54 const enhance = compose( checkAuthorization, showSpinnerWhileLoading, branch(({ products })

    => !products.length, renderComponent(NoData)) ) const ProductList = ({ products }) => (...) export default enhance(ProductList) Еще раз про граничные условия
  55. recompose •  https://github.com/acdlite/recompose •  есть все функции, которые мы реализовали,

    и множество других •  почти 10 000 звёд, хорошая поддержка •  flow •  RxJS •  TypeScript •  React Native 55
  56. Простое e-commerce приложение 56 AddToCartButton – компонент, отвечающий за отрисовку

    кнопки для добавления товара в корзину
  57. withHandlers 57 const AddToCartButton = ({ price, productId, onAddToCart })

    => ( <div> <span>{price}</span> <button onClick={() => onAddToCart(productId)}> Add to cart </button> </div> )
  58. withHandlers 58 const AddToCartButton = ({ price, productId, onAddToCart })

    => ( <div> <span>{price}</span> <button onClick={() => onAddToCart(productId)}> Add to cart </button> </div> )
  59. withHandlers 59 const AddToCartButton = ({ price, productId, onAddToCart })

    => ( <div> <span>{price}</span> <button onClick={() => onAddToCart(productId)}> Add to cart </button> </div> )
  60. withHandlers 60 const enhance = withHandlers({ onClickHandler: ({ onAddToCart, productId

    }) => () => onAddToCart(productId) }) const AddToCartButton = ({ price, onClickHandler }) => ( <div> <span>{price}</span> <button onClick={onClickHandler}>Add to cart</button> </div> ) export default enhance(AddToCartButton)
  61. withHandlers 61 const enhance = withHandlers({ onClickHandler: ({ onAddToCart, productId

    }) => () => onAddToCart(productId) }) const AddToCartButton = ({ price, onClickHandler }) => ( <div> <span>{price}</span> <button onClick={onClickHandler}>Add to cart</button> </div> ) export default enhance(AddToCartButton)
  62. withHandlers 62 const enhance = withHandlers({ onClickHandler: ({ onAddToCart, productId

    }) => () => onAddToCart(productId) }) const AddToCartButton = ({ price, onClickHandler }) => ( <div> <span>{price}</span> <button onClick={onClickHandler}>Add to cart</button> </div> ) export default enhance(AddToCartButton)
  63. withHandlers 63 const enhance = withHandlers({ onClickHandler: ({ onAddToCart, productId

    }) => () => onAddToCart(productId) }) const AddToCartButton = ({ price, onClickHandler }) => ( <div> <span>{price}</span> <button onClick={onClickHandler}>Add to cart</button> </div> ) export default enhance(AddToCartButton)
  64. Простое e-commerce приложение 64 ProductListContainer – компонент, отвечающий за загрузку

    списка товаров
  65. Загрузка данных 65 class ProductListContainer extends Component { constructor(props) {

    /* инициализируем cтейт */ } loadProducts = () => { /* идем на сервер за данными */ } componentWillMount() { this.loadProducts() } render() { /* рисуем */ } }
  66. Загрузка данных 66 class ProductListContainer extends Component { constructor(props) {

    super(props) this.state = { products: [], loading: false } } //... }
  67. Загрузка данных 67 class ProductListContainer extends Component { loadProducts =

    () => { this.setState({ loading: true }) fetchProducts().then(products => this.setState({ products, loading: false }) ) } //... }
  68. Загрузка данных 68 class ProductListContainer extends Component { render() {

    return ( <ProductList products={this.state.products} loading={this.state.loading} /> ) } //... }
  69. class ProductListContainer extends Component { constructor(props) { super(props) this.state =

    { products: [], loading: false } } loadProducts = () => { this.setState({ loading: true }) fetchProducts().then(products => this.setState({ products, loading: false }) ) } componentWillMount() { this.loadProducts() } render() { const { products, loading } = this.state return (<ProductList products={products} loading={loading} />) } } 69
  70. 70 const enhance = compose( withState('products', 'setProducts', []), withState('loading', 'setLoading',

    false), withHandlers({ loadProducts: ({ setLoading, setProducts }) => () => { setLoading(true) fetchProducts().then(products => { setLoading(false) setProducts(products) }) } }), lifecycle({ componentWillMount() { this.props.loadProducts() } }) ) export default enhance(ProductList)
  71. 71 const enhance = compose( withState('products', 'setProducts', []), withState('loading', 'setLoading',

    false), withHandlers({ loadProducts: ({ setLoading, setProducts }) => () => { setLoading(true) fetchProducts().then(products => { setLoading(false) setProducts(products) }) } }), lifecycle({ componentWillMount() { this.props.loadProducts() } }) ) export default enhance(ProductList)
  72. 72 const enhance = compose( withState('products', 'setProducts', []), withState('loading', 'setLoading',

    false), withHandlers({ loadProducts: ({ setLoading, setProducts }) => () => { setLoading(true) fetchProducts().then(products => { setLoading(false) setProducts(products) }) } }), lifecycle({ componentWillMount() { this.props.loadProducts() } }) ) export default enhance(ProductList)
  73. Простое e-commerce приложение 73 NavbarItem – компонент, отвечающий за отрисовку

    элемента навигационного меню
  74. componentFromProp 74 <Navbar> <NavbarItem to={homePath}>Home</NavbarItem> <NavbarItem to={cartPath}>Cart</NavbarItem> <NavbarItem onClick={signOut}>Sign Out</NavbarItem>

    </Navbar>
  75. 75 const NavbarItem = ({ to, onClick, children }) =>

    { const className = 'navbar-item' if (to) { return ( <Link className={className} to={to}>{children}</Link> ) } if (onClick) { return ( <button className={className} onClick={onClick}>{children}</button> ) } return null }
  76. 76 const enhance = compose( withProps({ className: 'navbar-item' }), branch(

    ({ to }) => to, withProps({ component: Link }) ), branch( ({ onClick }) => onClick, withProps({ component: 'button' }) ) ) export default enhance(componentFromProp('component'))
  77. 77 const enhance = compose( withProps({ className: 'navbar-item' }), branch(

    ({ to }) => to, withProps({ component: Link }) ), branch( ({ onClick }) => onClick, withProps({ component: 'button' }) ) ) export default enhance(componentFromProp('component'))
  78. redux + загрузка данных •  запрос выполняется вне компонента • 

    локального состояния нет •  не перерисовывать компонент, если ничего не изменилось •  по возможности – брать данные из store 78
  79. const mapStateToProps = (state, { match: { params: { id

    } } }) => ({ product: getProduct(state, id) }) class ProductProfileContainer extends Component { shouldComponentUpdate(nextProps) { return nextProps.match.params.id !== this.props.match.params.id } componentDidMount() { requestProduct(this.props.match.params.id) } render() { return <ProductProfile product={this.props.product} /> } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 79
  80. const mapStateToProps = (state, { match: { params: { id

    } } }) => ({ product: getProduct(state, id) }) class ProductProfileContainer extends Component { shouldComponentUpdate(nextProps) { return nextProps.match.params.id !== this.props.match.params.id } componentDidMount() { requestProduct(this.props.match.params.id) } render() { return <ProductProfile product={this.props.product} /> } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 80
  81. const mapStateToProps = (state, { match: { params: { id

    } } }) => ({ product: getProduct(state, id) }) class ProductProfileContainer extends Component { shouldComponentUpdate(nextProps) { return nextProps.match.params.id !== this.props.match.params.id } componentDidMount() { requestProduct(this.props.match.params.id) } render() { return <ProductProfile product={this.props.product} /> } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 81
  82. const mapStateToProps = (state, { match: { params: { id

    } } }) => ({ product: getProduct(state, id) }) class ProductProfileContainer extends Component { shouldComponentUpdate(nextProps) { return nextProps.match.params.id !== this.props.match.params.id } componentDidMount() { requestProduct(this.props.match.params.id) } render() { return <ProductProfile product={this.props.product} /> } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 82
  83. const mapStateToProps = (state, { match: { params: { id

    } } }) => { id, product: getProduct(state, id) } class ProductProfileContainer extends Component { shouldComponentUpdate(nextProps) { return nextProps.match.params.id !== this.props.id } componentDidMount() { requestProduct(this.props.id) } render() { return <ProductProfile product={this.props.product} /> } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 83
  84. const mapStateToProps = (state, { match: { params: { id

    } } }) => { id, product: getProduct(state, id) } class ProductProfileContainer extends Component { shouldComponentUpdate(nextProps) { return nextProps.match.params.id !== this.props.id } componentDidMount() { requestProduct(this.props.id) } render() { return <ProductProfile product={this.props.product} /> } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 84
  85. const mapStateToProps = (state, { match: { params: { id

    } } }) => { id, product: getProduct(state, id) } class ProductProfileContainer extends Component { shouldComponentUpdate(nextProps) { return nextProps.match.params.id !== this.props.id } componentDidMount() { requestProduct(this.props.id) } render() { return <ProductProfile product={this.props.product} /> } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 85
  86. 86 const mapStateToProps = (state, { match: { params: {

    id } } }) => ({ id, product: getProduct(state, id) }) const enhance = compose( onlyUpdateForPropTypes, setPropTypes({ product: ProductShape }), connect(mapStateToProps, mapDispatchToProps), lifecycle({ componentDidMount() { requestProduct(this.props.id) } }) ) export default enhance(ProductProfile)
  87. 87 const mapStateToProps = (state, { match: { params: {

    id } } }) => ({ id, product: getProduct(state, id) }) const enhance = compose( onlyUpdateForPropTypes, setPropTypes({ product: ProductShape }), connect(mapStateToProps, mapDispatchToProps), lifecycle({ componentDidMount() { requestProduct(this.props.id) } }) ) export default enhance(ProductProfile)
  88. 88 const mapStateToProps = (state, { match: { params: {

    id } } }) => ({ id, product: getProduct(state, id) }) const enhance = compose( onlyUpdateForPropTypes, setPropTypes({ product: ProductShape }), connect(mapStateToProps, mapDispatchToProps), lifecycle({ componentDidMount() { requestProduct(this.props.id) } }) ) export default enhance(ProductProfile)
  89. Альтернативы recompose •  написать руками •  Recompact 38.2kB minified, 6.9kB

    gzipped, 260 звёзд (https:// github.com/neoziro/recompact) •  Reassemble 21.7kB minified, 4.5kB gzipped, 54 звезды (https://github.com/wikiwi/reassemble) 89
  90. Бенчмарки 90 100 HOCs, измеряем число обновлений в секунду https://github.com/neoziro/recompact/tree/master/src/__benchmarks__

  91. Производительность •  производительность может ухудшиться из-за появления новых компонентов в

    дереве •  легче управлять перерисовкой компонентов •  многие компоненты реализованы в виде чистых функций (без использования классов), в будущем возможна оптимизация работы таких компонентов в React 91
  92. Тестирование 92 const withProductCount = withProps( ({ products }) =>

    ({ count: products.length }) ) describe('withProductCount decorator', () => { const products = [{ id: 1 }] const MockComponent = () => <div /> it('adds product count prop', () => { const Enhanced = withProductCount(MockComponent) const wrapper = mount(<Enhanced products={products} />) const subject = wrapper.find(MockComponent) expect(subject.prop('count')).toBe(1) }) })
  93. Тестирование 93 const withProductCount = withProps( ({ products }) =>

    ({ count: products.length }) ) describe('withProductCount decorator', () => { const products = [{ id: 1 }] const MockComponent = () => <div /> it('adds product count prop', () => { const Enhanced = withProductCount(MockComponent) const wrapper = mount(<Enhanced products={products} />) const subject = wrapper.find(MockComponent) expect(subject.prop('count')).toBe(1) }) })
  94. Тестирование 94 const withProductCount = withProps( ({ products }) =>

    ({ count: products.length }) ) describe('withProductCount decorator', () => { const products = [{ id: 1 }] const MockComponent = () => <div /> it('adds product count prop', () => { const Enhanced = withProductCount(MockComponent) const wrapper = mount(<Enhanced products={products} />) const subject = wrapper.find(MockComponent) expect(subject.prop('count')).toBe(1) }) })
  95. Тестирование 95 const withProductCount = withProps( ({ products }) =>

    ({ count: products.length }) ) describe('withProductCount decorator', () => { const products = [{ id: 1 }] const MockComponent = () => <div /> it('adds product count prop', () => { const Enhanced = withProductCount(MockComponent) const wrapper = mount(<Enhanced products={products} />) const subject = wrapper.find(MockComponent) expect(subject.prop('count')).toBe(1) }) })
  96. const withGreeting = compose( setDisplayName('withGreeting'), setPropTypes({ greeting: PropTypes.string.required, children: PropTypes.string.required

    }), onlyUpdateForPropTypes, mapProps(({ greeting, children }) => ({ text: `${greeting}, ${children}` })) ) const Label = ({ text }) => (<h1>{text}</h1>) const GreetingLabel = withGreeting(Label) const App = () => <GreetingLabel greeting="Hi">John</GreetingLabel> Отладка
  97. const withGreeting = compose( setDisplayName('withGreeting'), setPropTypes({ greeting: PropTypes.string.required, children: PropTypes.string.required

    }), onlyUpdateForPropTypes, mapProps(({ greeting, children }) => ({ text: `${greeting}, ${children}` })) ) const Label = ({ text }) => (<h1>{text}</h1>) const GreetingLabel = withGreeting(Label) const App = () => <GreetingLabel greeting="Hi">John</GreetingLabel> Отладка
  98. const withGreeting = compose( setDisplayName('withGreeting'), setPropTypes({ greeting: PropTypes.string.required, children: PropTypes.string.required

    }), onlyUpdateForPropTypes, mapProps(({ greeting, children }) => ({ text: `${greeting}, ${children}` })) ) const Label = ({ text }) => (<h1>{text}</h1>) const GreetingLabel = withGreeting(Label) const App = () => <GreetingLabel greeting="Hi">John</GreetingLabel> Отладка
  99. const withGreeting = compose( setDisplayName('withGreeting'), setPropTypes({ greeting: PropTypes.string.required, children: PropTypes.string.required

    }), onlyUpdateForPropTypes, mapProps(({ greeting, children }) => ({ text: `${greeting}, ${children}` })) ) const Label = ({ text }) => (<h1>{text}</h1>) const GreetingLabel = withGreeting(Label) const App = () => <GreetingLabel greeting="Hi">John</GreetingLabel> Отладка
  100. Зачем нам HOC? •  убирать логику из presentational components и

    описать ее декларативно •  повторно использовать presentational components в разных частях приложения •  тестировать логику отдельно от представления 100
  101. Что может пойти не так? •  нельзя использовать HOC внутри

    render (ок, не будем) •  статические методы придется копировать •  с refs работать неудобно (но можно) •  коллизии имен 101 https://reactjs.org/docs/higher-order-components.html#caveats
  102. Зачем нам recompose? •  не писать кучу boilerplate-кода •  удобно

    управлять перерисовкой компонентов •  класть в props обработчики и переменные по мере необходимости •  удобно обрабатывать граничные условия с помощью branch 102
  103. А как же Render Props? 103

  104. class ProductListContainer extends Component { constructor(props) { super(props) this.state =

    { products: [], loading: false } } loadProducts = () => { this.setState({ loading: true }) fetchProducts().then(products => this.setState({ products, loading: false }) ) } componentWillMount() { this.loadProducts() } render() { const { products, loading } = this.state return (<ProductList products={products} loading={loading} />) } } 104
  105. class ProductList extends Component { ... render() { return this.props.render(this.state)

    } } <ProductList render={ ({ products }) => (<table>...</table>) } /> 105
  106. Спасибо за внимание! dmitry.a.tsepelev@gmail.com @dmitrytsepelev DmitryTsepelev