React Elements против React Components

React Elements против React Components
mangohost

Несколько месяцев назад я думал, что запостил довольно простой вопрос в Twitter.

Я тщательно подхожу к изучению языка, но у меня до сих пор нет хорошего определения для компонента (React компонента). Есть идеи?

Скриншот из твиттера

Меня удивила не общая путаница в этом вопросе, а количество неточных ответов, которые я получил:

Instances / Instantiation
Rendering
Evaluation
Invocation
“Using it :)”

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

Давайте начнём с рассмотрения основ React. Что такое React? Это библиотека для разработки пользовательских интерфейсов. Как бы сложно React и его экосистема не выглядела, это React и его суть - разработка UI. С этой мыслью, мы подходим к нашему первому определению, Элемент (Element). Простыми словами, React элемент описывает то что вы хотите увидеть экране. А если не простыми, то React элемент описывает узел DOM в виде объекта. Обратите внимание на то, что я использовал слово описывает. Важно, что React элемент это не то что вы увидите на экране, а он описывает то что вы увидите. Для этого есть несколько причин. Первая причина заключается в том, что JavaScript объекты достаточно лёгкие и React может создавать и уничтожать их без слишком большого оверхэда. Вторая причина заключается в том, что React может анализировать объект и анализировать настоящий DOM, а затем обновлять DOM только в том месте, где произошли изменения. Это даёт некоторые плюсы в плане производительности.

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

const element = React.createElement(
  'div',
  {id: 'login-btn'},
  'Login'
)

createElement принимает три аргумента. Первый это название тега (div, span и т.д.), второй это атрибуты которые вы хотите задать элементу и третий это содержимое или дочерний элемент, в нашем случае это текст "Login". createElement вызываемый выше вернёт следующий объект:

{
  type: 'div',
  props: {
    children: 'Login',
    id: 'login-btn'
  }
}

и когда он будет отображён в DOM (с помощью функции ReactDOM.render), у нас появиться новый DOM узел, который будет выглядеть следующим образом:

<div id='login-btn'>Login</div>

Пока что всё хорошо. Что действительно интересно в изучении React, это то что как правило учат в первую очередь - компоненты. Компоненты - это строительные блоки в React. Однако, заметьте что мы начали эту статью с элементов. Причина по которой мы это сделали, заключается в том, что если вы поймёте элементы, то плавно перейдёте к пониманию компонентов. React компонент - это функция или класс, который принимает входные данные (опционально) и возвращает React элемент.

function Button ({ onLogin }) {
  return React.createElement(
    'div',
    {id: 'login-btn', onClick: onLogin},
    'Login'
  )
}

По  определению, у нас есть компонент Button, который принимает функцию onLogin и возвращает React элемент. Надо отметить, что компонент Button получает функцию onLogin в качестве своего свойства. Чтобы передать её в наш объект описывающий DOM, мы передаём её в качестве второго аргумента метода createElement, также как мы передавали наш атрибут id.

Давайте углубимся.

До этого момента, мы создавали элементы только с помощью нативных HTML элементов (div, span и т.д.), но мы также можем передавать другие React компоненты в качестве первого аргумента функции createElement.

const element = React.createElement(
  User,
  {name: 'Tyler McGinnis'},
  null
)

Однако, в отличии от имени HTML тега, если React увидит класс или функцию в качестве первого аргумента, он проверит какой элемент будет отображаться, учитывая соответствующие свойства. React будет рекурсивно продолжать делать это, пока не останется вызовов createElement без функций или классов в качестве первого аргумента. Давайте посмотрим на это в действии.

function Button ({ addFriend }) {
  return React.createElement(
    "button",
    { onClick: addFriend },
    "Add Friend"
  )
}

function User({ name, addFriend }) {
  return React.createElement(
    "div",
    null,
    React.createElement(
      "p",
      null,
      name
    ),
    React.createElement(Button, { addFriend })
  )
}

В примере выше, у нас есть два компонента, Button и User. Объект User будет отображён как элемент "div" с двумя дочерними элементами, "p" который оборачивает имя пользователя и компонент Button. Теперь давайте заменим вызовы функции createElement на то что они возвращают:

function Button ({ addFriend }) {
  return {
    type: 'button',
    props: {
      onClick: addFriend,
      children: 'Add Friend'
    }
  }
}

function User ({ name, addFriend }) {
  return {
    type: 'div',
    props: {
      children: [
        {
          type: 'p',
          props: {
            children: name
          }
        },
        {
          type: Button,
          props: {
            addFriend
          }
        }
      ]
    }
  }
}

Вы заметили, что вы приведённом выше примере у нас есть 4 разных свойства "type" - "button", "div", "p", and Button. Когда React увидит элемент с функцией или классом в типе (такой как "type: Button" выше), он обратиться к этому компоненту, чтобы узнать что он возвращает, учитывая соответствующие свойства. В конце этого процесса у React есть полный объект представления дерева DOM. В нашем примере оно выглядит следующим образом:

{
  type: 'div',
  props: {
    children: [
      {
        type: 'p',
        props: {
          children: 'Tyler McGinnis'
        }
      },
      {
        type: 'button',
        props: {
          onClick: addFriend,
          children: 'Add Friend'
        }
      }
    ]
  }
}

Весь этот процесс в React называется "сверка" (reconciliation) и срабатывает каждый раз, когда вызываются функции setState или ReactDOM.render.

Теперь давайте опять взглянем на наш первоначальный вопрос:

Скриншот из твиттера

На данный момент у нас есть все необходимые знания для ответа на этот вопрос, за исключением одной важной части. Скорей всего, если вы используете React уже на протяжении какого-то времени, то вы не используете функцию React.createElement для создания объектов описывающих DOM. Вместо этого, вероятно вы используете JSX. Ранее я писал, что "Такая путаница происходит из-за того, что мы часто не говорим об абстрактном слое между JSX и тем, что на самом деле происходит в React". Этот абстрактный слой заключается в том, что JSX всегда компилируется в вызов React.createElement с помощью Babel.

Взгляните на код, из нашего предыдущего примера:

function Button ({ addFriend }) {
  return React.createElement(
    "button",
    { onClick: addFriend },
    "Add Friend"
  )
}

function User({ name, addFriend }) {
  return React.createElement(
    "div",
    null,
    React.createElement(
      "p",
      null,
      name
    ),
    React.createElement(Button, { addFriend })
  )
}

Это результат компиляции следующего JSX кода:

function Button ({ addFriend }) {
  return (
    <button onClick={addFriend}>Add Friend</button>
  )
}

function User ({ name, addFriend }) {
  return (
    <div>
      <p>{name}</p>
      <Button addFriend={addFriend}/>
    </div>
  )
}

И наконец, как называется действие, когда напишем наш компонент как этот, <Icon />?

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

React.createElement(Icon, null)

Все примеры, "создания React элемента":

React.createElement('div', className: 'container', 'Hello!')

<div className='container'>Hello!</div>

<Hello />

Для получения более полной информации прочитайте статью "React Components, Instances, and Elements" от Dan Abramov.

mangohost