Паттерны React

в 12:17, , рубрики: patterns, ReactJS, technics

Привет! Предлагаю вашему вниманию свободный перевод статьи «React Patterns» Майкла Чана, с некоторыми моими примечаниями и дополнениями.

Прежде всего хотел бы поблагодарить автора оригинального текста. В переводе я использовал понятие «Простой компонент» как обозначение Stateless Component aka Dump Component aka Component vs Container
Конструктивная критика, а так же альтернативные паттерны и фичи React приветствуются в комментах.

Оглавление

  • Простые компоненты — Stateless function
  • JSX распределение атрибутов — JSX Spread Attributes
  • Деструктуризация аргументов — Destructuring Arguments
  • Условный рендеринг — Conditional Rendering
  • Типы потомков — Children Types
  • Массив как потомок — Array as children
  • Функция как потомок — Function as children
  • Функция в render — Render callback
  • Проход по потомкам — Children pass-through
  • Перенаправление компонента — Proxy component
  • Стилизация компонентов — Style component
  • Переключатель событий — Event switch
  • Компонент-макет — Layout component
  • Компонент-контейнер — Container component
  • Компоненты высшего порядка — Higher-order component

Поехали!

Stateless function

Функция без состояния ( далее Простые Копоненты) прекрасный способ определить универсальный компонент. Они не содержат состояния (state) или ссылку на DOM элемент (ref), это просто функции.

const Greeting = () => <div>Hi there!</div>

В них передаются параметры (props) и контекст

const Greeting = (props, context) =>
  <div style={{color: context.color}}>Hi {props.name}!</div>

Они могут определять локальные переменные, если используете блоки ({})

const Greeting = (props, context) => {
  const style = {
    fontWeight: "bold",
    color: context.color,
  }
  return <div style={style}>{props.name}</div>
}

Но вы можете получить тот же результат, если используете еще одну функцию

const getStyle = context => ({
  fontWeight: "bold",
  color: context.color,
})
const Greeting = (props, context) =>
  <div style={getStyle(context)}>{props.name}</div>

Они могут определить defaultProps, propTypes и contextTypes

Greeting.propTypes = {
  name: PropTypes.string.isRequired
}
Greeting.defaultProps = {
  name: "Guest"
}
Greeting.contextTypes = {
  color: PropTypes.string
}

JSX Spread Attributes

Распределение атрибутов это фитча JSX. Такой синтаксический наворот, чтобы передавать все свойства объекта как атрибуты JSX

Эти два примера эквивалентны:

— props написаны как атрибуты:

<main className="main" role="main">{children}</main>

— props «распределены» из объекта:

<main {...{className: "main", role: "main", children}} />

Используйте перенаправление props в создаваемый объект

const FancyDiv = props =>
  <div className="fancy" {...props} />

Теперь вы можете быть уверены, что нужный атрибут будет присутствовать (className), так же как и те которые вы не указали напрямую в функции а передали в нее вместе с props

<FancyDiv data-id="my-fancy-div">So Fancy</FancyDiv>

Результат:

<div className="fancy" data-id="my-fancy-div">So Fancy</div>

Имейте ввиду, что порядок имеет значение. если props.className определено, то это свойство перепишет className определенное в FancyDiv

<FancyDiv className="my-fancy-div" />

Результат:

<div className="my-fancy-div"></div>

We can make FancyDivs className always “win” by placing it after the spread props ({…props}).
Вы можете сделать так, что ваше свойство всегда перепишет переданные через props

const FancyDiv = props =>
  <div {...props} className="fancy" />

Есть более изящный подход — объединить оба свойства.

const FancyDiv = ({ className, ...props }) =>
  <div
    className={["fancy", className].join(' ')}
    {...props}
  />

Destructuring Arguments

Деструктурирующее присвоение это фича стандарта ES2015. Она отлично сочетается с props для Простых Компонентов.

Эти примеры эквивалентны

const Greeting = props => <div>Hi {props.name}!</div>
const Greeting = ({ name }) => <div>Hi {name}!</div>

Синтаксис оператора rest (…) позволяет собрать оставшиеся свойства в объект

const Greeting = ({ name, ...props }) =>
  <div>Hi {name}!</div>

Далее этот объект может быть использован для передачи не выделенных свойств далее в созданном компоненте

const Greeting = ({ name, ...props }) =>
  <div {...props}>Hi {name}!</div>

Avoid forwarding non-DOM props to composed components. Destructuring makes this very easy because you can create a new props object without component-specific props.

Conditional Rendering

Можете использовать обычный if/else синтаксис в компонентах. Но условные (тернарные) операторы это ваши друзья

if

{condition && <span>Rendered when `truthy`</span> }

unless

{condition || <span>Rendered when `falsey`</span> }

if-else (tidy one-liners)

{condition
  ? <span>Rendered when `truthy`</span>
  : <span>Rendered when `falsey`</span>
}

if-else (big blocks)

{condition ? (
  <span>
    Rendered when `truthy`
  </span>
) : (
  <span>
    Rendered when `falsey`
  </span>
)}

* Я предпочитаю не использовать конструкции из последнего примера, гораздо нагляднее в данном случае будет использование обычного if/else, хотя все зависит от конкретного кода.

Children Types

React может рендерить потомков любого типа. В основном это массив или строка

Строка

<div>
  Hello World!
</div>

Массив

<div>
  {["Hello ", <span>World</span>, "!"]}
</div>

Функции могут быть так же использованы как потомки. Однако, нужно координировать их поведение с родительским компонентом.

Функция

<div>
  {() => { return "hello world!"}()}
</div>

Array as children

Использование массива потомков, это обычный паттерн, например так вы делаете списки в React.

Используйте map(), чтобы сделать массив элементов React, для каждого значения в массиве.

<ul>
  {["first", "second"].map((item) => (
    <li>{item}</li>
  ))}
</ul>

Это эквивалентно литералу массива с объектами

<ul>
  {[
    <li>first</li>,
    <li>second</li>,
  ]}
</ul>

Такой паттерн может быть использован совместно с деструктуризацией, распределением атрибутов и другими фичами, чтобы упростить написание кода

<ul>
  {arrayOfMessageObjects.map(({ id, ...message }) =>
    <Message key={id} {...message} />
  )}
</ul>

Function as children

Использование функций как потомков требует дополнительного внимания с вашей стороны, чтобы можно было извлечь из них пользу.

<div>{() => { return "hello world!»}()}</div>

Однако, они могут придать вашим компонентам супер силу, такая техника обычно называется рендер-коллбэк.

Эта мощная техника используется в таких библиотеках как ReactMotion. Когда вы применяете ее, логика рендера может управляйся из родительского компонента, вместо того, чтобы полностью передать ее самому компоненту.

Render callback

Вот пример компонента который использует рендер-коллбэк. Он в целом бесполезен, однако это хорошая иллюстрация возможностей, для начала.

const Width = ({ children }) => children(500)

Компонент вызывает потомков, как функцию с определенным аргументом. В данном случае это число 500.

Чтобы использовать этот компонент мы передаем ему функцию как потомка.

<Width>
  {width => <div>window is {width}</div>}
</Width>

Получим такой результат

<div>window is 500</div>

При таком подходе, можно использовать параметр (width), для условного рендеринга

<Width>
  {width =>
    width > 600
      ? <div>min-width requirement met!</div>
      : null
  }
</Width>

Если планируем использовать такое условие много раз, то мы можем определить другой компонент, чтобы передать ему эту логику

const MinWidth = ({ width: minWidth, children }) =>
  <Width>
    {width =>
      width > minWidth
        ? children
        : null
    }
  </Width>

Очевидно, что статичный компонент Width, не очень полезен, но мы можем наблюдать за размерами окна браузера при таком подходе, это уже что-то

class WindowWidth extends React.Component {
  constructor() {
    super()
    this.state = { width: 0 }
  }

  componentDidMount() {
    this.setState(
      {width: window.innerWidth},
      window.addEventListener(
        "resize",
        ({ target }) =>
          this.setState({width: target.innerWidth})
      )
    )
  }

  render() {
    return this.props.children(this.state.width)
  }
}

Многие предпочитают Компоненты Высшего Порядка для такого типа функционала. Это вопрос личных преференций.

Children pass-through

Вы можете создать компонент, чтобы применить контекст и рендерить потомков.

class SomeContextProvider extends React.Component {
  getChildContext() {
    return {some: "context"}
  }

  render() {
    // how best do we return `children`?
  }
}

Тут вам следует принять решение. Обернуть потомков в еще один html тэг (div), или вернуть только потомков. Первый вариант может повлиять на существующую разметку и может нарушить стили. Второй — приведет к ошибке (вы помните, что можно вернуть только один родительский элемент из компонента)

Вариант 1: дополнительный div

return <div>{children}</div>

Вариант 2: ошибка

return children

Лучше всего управлять потомками при помощи специальных методов — React.Children. Например пример ниже позволяет вернуть только потомков и не требует дополнительной обертки

return React.Children.only(this.props.children)

Proxy component

(Не уверен, что это название вообще что-то значит) прим. автора статьи

Кнопки повсюду в приложении. И каждая из них должна иметь атрибут типа ‘button’

<button type=«button">

Писать такое сотни раз ручками — не наш метод. Мы можем написать компонент более высокого уровня, чтобы перенаправить props в компонент уровнем ниже.

const Button = props =>
  <button type="button" {…props}>

Далее вы просто используете Button, вместо button, и можете быть уверены, что нужный атрибут будет присутствовать в каждой кнопке.

<Button />
// вернет <button type="button"><button>

<Button className="CTA">Send Money</Button>
// вернет <button type="button" class="CTA">Send Money</button>

Style component

Это Proxy Component примененный к стилям. Скажем, у нас есть кнопка. Она использует классы, чтобы выглядеть как ‘primary’.

<button type="button" className="btn btn-primary»>

Можно замутить такое используя несколько простых компонентов

const PrimaryBtn = props =>
  <Btn {...props} primary />

const Btn = ({ className, primary, ...props }) =>
  <button
    type="button"
    className={classnames(
      "btn",
      primary && "btn-primary",
      className
    )}
    {...props}
  />

Это поможет визуализировать происходящее
PrimaryBtn()
↳ Btn({primary: true})
↳ Button({className: «btn btn-primary»}, type: «button»})
↳ '<button type=«button» class=«btn btn-primary»>'

Использование этих компонентов вернет одинаковый результат

<PrimaryBtn />
<Btn primary />
<button type="button" className="btn btn-primary" />

Такой подход может принести ощутимую пользу, так как изолирует определенные стили в определенном компоненте.

Event switch

При написании обработчиков событий, обычно мы используем соглашение о названии функций

handle{eventName}
handleClick(e) { /* do something */ }

Для компонентов, которые обрабатывают несколько событий, такое наименование может быть излишне повторяющемся. Сами по себе названия не дают нам никакой ценности, так как они просто направляют к действиям/функциям

handleClick() { require("./actions/doStuff")(/* action dtes */) }
handleMouseEnter() { this.setState({ hovered: true }) }
handleMouseLeave() { this.setState({ hovered: false }) }

Давайте напишем простой обработчик для всех событий с переключателем по типу события (event.type)

handleEvent({type}) {
  switch(type) {
    case "click":
      return require("./actions/doStuff")(/* action dates */)
    case "mouseenter":
      return this.setState({ hovered: true })
    case "mouseenter":
      return this.setState({ hovered: false })
    default:
      return console.warn(`No case for event type "${type}"`)
  }
}

Можно так же вызывать функции аргументы напрямую, используя функцию-стрелку

<div onClick={() => someImportedAction({ action: "DO_STUFF" })}

Не парьтесь насчет оптимизации производительности, пока не столкнулись с такими проблемами. Серьезно, не нужно.

* Лично я не считаю такой подход удачным, тк он не добавляет читаемости коду. Я предпочитаю использовать фичи React c функциями которые привязываются к контексту автоматом. То есть следующая нотация более не является необходимостью

this.handleClick = this.handleClick.bind(this)

вместо нее работает следующая нотация

handleClick = () => {…}  // вместо handleClick() {...}

и далее где-то возможно просто

onClick={this.handleClick}

В таком случае контекст (this) не будет утерян, если внутри функция обработчик ссылается к нему. Соответственно, такие функции можно легко передавать в качестве props другим компонентам и вызывать в них.

Также, в случае, если мы передаем такую функцию в потомок, Простой Компонент, то можем получить ссылку на DOM элемент этого компонента через event.target в родительском компоненте, что иногда полезно.

class SomeComponent extends React.Component {	
  onButtonClick = (e) => {
     const button = e.target;
     // …
  }
  render() {
   <div>
      <Input … />
      <Button clickHandler={this.onButtonClick} />
   </div>
  }
}

const Button = ({clickHandler, …props}) => {
  const btnClickHandler = (e) => {
    // что-то происходит
    e.preventDefault()
    clickHandler(e)
  }
  return <button onClick={btnClickHandler}/>
}

Layout component

Компоненты макета это что-то вроде статических элементов DOM. Скоро всего они не будут обновляться часто, если будут вообще.

Рассмотрим компонент который содержит два компонента горизонтально.

<HorizontalSplit
  leftSide={<SomeSmartComponent />}
  rightSide={<AnotherSmartComponent />}
/>

Мы можем агрессивно оптимизировать его работу.

Так как HorizontalSplit будет родительским компонентом для обоих компонентов он никогда не станет их владельцем. Мы можем сказать чтобы он никогда не обновлялся, при этом не прорывая жизненные циклы внутренних компонентов.

class HorizontalSplit extends React.Component {
  shouldComponentUpdate() {
    return false
  }
  render() {
    <FlexContainer>
      <div>{this.props.leftSide}</div>
      <div>{this.props.rightSide}</div>
    </FlexContainer>
  }

Container component

“A container does data fetching and then renders its corresponding sub-component. That’s it.” — Jason Bonta

Дано: компонент CommentList, который используется несколько раз в приложении.

const CommentList = ({ comments }) =>
  <ul>
    {comments.map(comment =>
      <li>{comment.body}-{comment.author}</li>
    )}
  </ul>

Мы можем создать новый компонент ответственный за получение данных и рендер компонента CommentList

class CommentListContainer extends React.Component {
  constructor() {
    super()
    this.state = { comments: [] }
  }

  componentDidMount() {
    $.ajax({
      url: "/my-comments.json",
      dataType: 'json',
      success: comments =>
        this.setState({comments: comments});
    })
  }

  render() {
    return <CommentList comments={this.state.comments} />
  }
}

Мы можем писать различные компоненты-контейнеры для разных контекстов приложения.

Higher-order component

Функция высшего порядка это функция которая может принимать в качестве аргументов другие функции и/или возвращать функции. Не более сложно чем данное определение. Так что такое компоненты высшего порядка?

Вы уже используете компоненты-контейнеры, это просто контейнеры, обернутые в функцию. Давайте начнем с простого Greeting компонента.

const Greeting = ({ name }) => {
  if (!name) { return <div>Connecting...</div> }
  return <div>Hi {name}!</div>
}

Если он получит props.name, он отрендерит данные. В противном случае он отрендерит “Connecting…”. Теперь немного более высокий порядок:

const Connect = ComposedComponent =>
  class extends React.Component {
    constructor() {
      super()
      this.state = { name: "" }
    }

    componentDidMount() {
      // this would fetch or connect to a store
      this.setState({ name: "Michael" })
    }

    render() {
      return (
        <ComposedComponent
          {...this.props}
          name={this.state.name}
        />
      )
    }
  }

Это функция которая возвращает компонент, который рендерит компонент, который мы передали в качестве аргумента (ComposedComponent)

Далее мы оборачиваем компоте в эту функцию.

const ConnectedMyComponent = Connect(Greeting)

Это очень мощный шаблон, чтобы компонент мог получать данные и раздавать их любому количеству простых компонентов.

Ссылки (все на английском):

Автор: montecazazza

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js