Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Почему компоненты распухают? •  перерисовка •  граничные условия •  загрузка данных •  подготовка props •  side-эффекты 2

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Перерисовка компонентов 8 const ProductRow = ({ id, name, price }) => ( {name} {price} { } Details )

Slide 9

Slide 9 text

Перерисовка компонентов •  pure •  проверяет shallowEqual •  может работать медленно, если state и props объемные •  ложно-положительные срабатывания •  shouldComponentUpdate •  реализуем сами •  дает больше контроля 9

Slide 10

Slide 10 text

Перерисовка компонентов 10 class ProductRow extends Component { shouldComponentUpdate(nextProps) { return nextProps.id !== this.props.id } render() { //... } }

Slide 11

Slide 11 text

Принцип разделения ответственности •  Эдсгер Дейкстра, “On the role of scientific thought”, 1974 •  Крис Рид, «Элементы функционального программирования», 1989 •  программа разделяется на секции, ответственные за определенные области бизнес-логики 11

Slide 12

Slide 12 text

Компонент высшего порядка (HOC) 12 const Name = () =>

Guest

const withGreeting = WrappedComponent => props => (
Hi,
) withGreeting(Name)

Slide 13

Slide 13 text

Компонент высшего порядка (HOC) 13 const Name = () =>

Guest

const withGreeting = WrappedComponent => props => (
Hi,
) withGreeting(Name)

Slide 14

Slide 14 text

Компонент высшего порядка (HOC) 14 const Name = () =>

Guest

const withGreeting = WrappedComponent => props => (
Hi,
) withGreeting(Name)

Slide 15

Slide 15 text

Компонент высшего порядка (HOC) 15 const Name = () =>

Guest

const withGreeting = WrappedComponent => props => (
Hi,
) withGreeting(Name)

Slide 16

Slide 16 text

onlyUpdateForKeys 16 const ProductRow = onlyUpdateForKeys(['id'])( ({ id, name, price /* ...больше свойств */ }) => ( {name} {price} Details ) )

Slide 17

Slide 17 text

onlyUpdateForKeys 17 const enhance = onlyUpdateForKeys(['id']) const ProductRow = ({ id, name, price /* ...больше свойств */ }) => ( {name} {price} Details ) export default enhance(ProductRow)

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Граничные условия в render 24 const ProductList = ({ products }) => ( Name Price {products.map(p => )} )

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

branch 32 const branch = (cond, cb) => WrappedComponent => props => cond(props) ? cb(props) :

Slide 33

Slide 33 text

branch 33 const branch = (cond, cb) => WrappedComponent => props => cond(props) ? cb(props) :

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Подготовка данных для отрисовки 35 const formattedPrice = price => `$${price}` const enhance = onlyUpdateForKeys(['id']) const ProductRow = ({ id, name, price }) => ( {name} {formattedPrice(price)} Details ) export default enhance(ProductRow)

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

withProps 37 const withProps = mapper => WrappedComponent => props => { const newProps = { ...props, ...mapper(props) } return }

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Композиция 39 withRouter( injectIntl( connect(mapStateToProps, mapDispatchToProps) ) )(LandingPage)

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

46 const ProductList = ({ currentUser, loading, products }) => { if (!currentUser) { return } if (loading) { return } if (!products.length) { return } return (...) } Еще раз про граничные условия

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

53 const showSpinnerWhileLoading = branch( ({ loading }) => loading, renderComponent(Spinner) ) Еще раз про граничные условия

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

recompose •  https://github.com/acdlite/recompose •  есть все функции, которые мы реализовали, и множество других •  почти 10 000 звёд, хорошая поддержка •  flow •  RxJS •  TypeScript •  React Native 55

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

withHandlers 57 const AddToCartButton = ({ price, productId, onAddToCart }) => (
{price} onAddToCart(productId)}> Add to cart
)

Slide 58

Slide 58 text

withHandlers 58 const AddToCartButton = ({ price, productId, onAddToCart }) => (
{price} onAddToCart(productId)}> Add to cart
)

Slide 59

Slide 59 text

withHandlers 59 const AddToCartButton = ({ price, productId, onAddToCart }) => (
{price} onAddToCart(productId)}> Add to cart
)

Slide 60

Slide 60 text

withHandlers 60 const enhance = withHandlers({ onClickHandler: ({ onAddToCart, productId }) => () => onAddToCart(productId) }) const AddToCartButton = ({ price, onClickHandler }) => (
{price} Add to cart
) export default enhance(AddToCartButton)

Slide 61

Slide 61 text

withHandlers 61 const enhance = withHandlers({ onClickHandler: ({ onAddToCart, productId }) => () => onAddToCart(productId) }) const AddToCartButton = ({ price, onClickHandler }) => (
{price} Add to cart
) export default enhance(AddToCartButton)

Slide 62

Slide 62 text

withHandlers 62 const enhance = withHandlers({ onClickHandler: ({ onAddToCart, productId }) => () => onAddToCart(productId) }) const AddToCartButton = ({ price, onClickHandler }) => (
{price} Add to cart
) export default enhance(AddToCartButton)

Slide 63

Slide 63 text

withHandlers 63 const enhance = withHandlers({ onClickHandler: ({ onAddToCart, productId }) => () => onAddToCart(productId) }) const AddToCartButton = ({ price, onClickHandler }) => (
{price} Add to cart
) export default enhance(AddToCartButton)

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Загрузка данных 68 class ProductListContainer extends Component { render() { return ( ) } //... }

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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)

Slide 71

Slide 71 text

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)

Slide 72

Slide 72 text

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)

Slide 73

Slide 73 text

Простое e-commerce приложение 73 NavbarItem – компонент, отвечающий за отрисовку элемента навигационного меню

Slide 74

Slide 74 text

componentFromProp 74 Home Cart Sign Out

Slide 75

Slide 75 text

75 const NavbarItem = ({ to, onClick, children }) => { const className = 'navbar-item' if (to) { return ( {children} ) } if (onClick) { return ( {children} ) } return null }

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

redux + загрузка данных •  запрос выполняется вне компонента •  локального состояния нет •  не перерисовывать компонент, если ничего не изменилось •  по возможности – брать данные из store 78

Slide 79

Slide 79 text

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 } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 79

Slide 80

Slide 80 text

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 } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 80

Slide 81

Slide 81 text

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 } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 81

Slide 82

Slide 82 text

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 } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 82

Slide 83

Slide 83 text

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 } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 83

Slide 84

Slide 84 text

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 } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 84

Slide 85

Slide 85 text

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 } } export default connect( mapStateToProps, mapDispatchToProps )(ProductProfileContainer) 85

Slide 86

Slide 86 text

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)

Slide 87

Slide 87 text

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)

Slide 88

Slide 88 text

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)

Slide 89

Slide 89 text

Альтернативы 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

Slide 90

Slide 90 text

Бенчмарки 90 100 HOCs, измеряем число обновлений в секунду https://github.com/neoziro/recompact/tree/master/src/__benchmarks__

Slide 91

Slide 91 text

Производительность •  производительность может ухудшиться из-за появления новых компонентов в дереве •  легче управлять перерисовкой компонентов •  многие компоненты реализованы в виде чистых функций (без использования классов), в будущем возможна оптимизация работы таких компонентов в React 91

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

const withGreeting = compose( setDisplayName('withGreeting'), setPropTypes({ greeting: PropTypes.string.required, children: PropTypes.string.required }), onlyUpdateForPropTypes, mapProps(({ greeting, children }) => ({ text: `${greeting}, ${children}` })) ) const Label = ({ text }) => (

{text}

) const GreetingLabel = withGreeting(Label) const App = () => John Отладка

Slide 97

Slide 97 text

const withGreeting = compose( setDisplayName('withGreeting'), setPropTypes({ greeting: PropTypes.string.required, children: PropTypes.string.required }), onlyUpdateForPropTypes, mapProps(({ greeting, children }) => ({ text: `${greeting}, ${children}` })) ) const Label = ({ text }) => (

{text}

) const GreetingLabel = withGreeting(Label) const App = () => John Отладка

Slide 98

Slide 98 text

const withGreeting = compose( setDisplayName('withGreeting'), setPropTypes({ greeting: PropTypes.string.required, children: PropTypes.string.required }), onlyUpdateForPropTypes, mapProps(({ greeting, children }) => ({ text: `${greeting}, ${children}` })) ) const Label = ({ text }) => (

{text}

) const GreetingLabel = withGreeting(Label) const App = () => John Отладка

Slide 99

Slide 99 text

const withGreeting = compose( setDisplayName('withGreeting'), setPropTypes({ greeting: PropTypes.string.required, children: PropTypes.string.required }), onlyUpdateForPropTypes, mapProps(({ greeting, children }) => ({ text: `${greeting}, ${children}` })) ) const Label = ({ text }) => (

{text}

) const GreetingLabel = withGreeting(Label) const App = () => John Отладка

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

Зачем нам recompose? •  не писать кучу boilerplate-кода •  удобно управлять перерисовкой компонентов •  класть в props обработчики и переменные по мере необходимости •  удобно обрабатывать граничные условия с помощью branch 102

Slide 103

Slide 103 text

А как же Render Props? 103

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

class ProductList extends Component { ... render() { return this.props.render(this.state) } } (...) } /> 105

Slide 106

Slide 106 text

Спасибо за внимание! [email protected] @dmitrytsepelev DmitryTsepelev