Компоненти і пропси

Компоненти дозволяють розділити інтерфейс користувача на незалежні частини, придатні до повторного використання, і сприймати їх як такі, що функціонують окремо один від одного. На цій сторінці викладений вступ до ідеї компонентів. Ви можете знайти докладний опис API компонентів тут.

Концептуально компоненти є подібними до функцій JavaScript. Вони приймають довільні вхідні дані (так звані “пропси”) і повертають React-елементи, що описують те, що повинно з’явитися на екрані.

Функціональні та класові компоненти

Найпростішим способом визначення компонента є написання функції JavaScript:

function Welcome(props) {
  return <h1>Привіт, {props.name}</h1>;
}

Ця функція є валідним React-компонентом, оскільки вона приймає єдиний аргумент “пропс” (скорочено від properties - властивості), який є об’єктом з даними і повертає React-елемент. Такі компоненти ми називаємо “функціональними компонентами”, оскільки вони буквально є JavaScript функціями.

Ви також можете використовувати ES6 класи, щоб визначити компонент:

class Welcome extends React.Component {
  render() {
    return <h1>Привіт, {this.props.name}</h1>;
  }
}

Два компоненти, що наведені вище, є еквівалентними з точки зору React.

Класи мають деякі додаткові особливості, які ми обговоримо в наступних розділах. До тих пір ми будемо використовувати функціональні компоненти через їх лаконічність.

Рендеринг компонентів

Раніше ми зустрічали лише React-елементи, які представляють теги DOM:

const element = <div />;

Однак елементи можуть також представляти визначені користувачем компоненти:

const element = <Welcome name="Василина" />;

Коли React бачить елемент, що представляє визначений користувачем компонент, він передає атрибути JSX цьому компоненту як єдиний об’єкт. Ми називаємо цей об’єкт “пропси”.

Наприклад, код нижче виводить на сторінці “Привіт, Василина”:

function Welcome(props) {
  return <h1>Привіт, {props.name}</h1>;
}

const element = <Welcome name="Василина" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

Спробуйте на CodePen

Давайте розберемо, що відбувається в цьому прикладі:

  1. Ми викликаємо ReactDOM.render() з елементом <Welcome name="Василина" />.
  2. React викликає компонент Welcome з пропсом {name: 'Василина'}.
  3. Welcome компонент повертає елемент <h1>Привіт, Василина</h1>.
  4. React DOM ефективно оновлює DOM для отримання <h1>Привіт, Василина</h1>.

Примітка: Завжди починайте писати імена компонентів з великої літери.

React розглядає компоненти, що починаються з малих літер, як теги DOM. Наприклад, <div /> представляє тег HTML div, але <Welcome /> являє собою компонент і вимагає, щоб Welcome знаходився в області застосування.

Щоб дізнатися більше про причини такої поведінки, прочитайте Поглиблений розгляд JSX.

Композиція компонентів

Компоненти можуть посилатися на інші компоненти під час виведення. Це дозволяє нам використовувати одну і ту ж абстракцію компонентів для будь-якого рівня деталізації. Кнопка, форма, діалогове вікно, екран: у React-додатках всі вони зазвичай виражаються як компоненти.

Наприклад, ми можемо створити компонент App, що відрендерить компонент Welcome багато разів:

function Welcome(props) {
  return <h1>Привіт, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="Василина" />
      <Welcome name="Михайло" />
      <Welcome name="Вадим" />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

Спробуйте на CodePen

Як правило, нові React-додатки мають єдиний компонент App, що знаходиться зверху дерева ієрархій елементів. Однак, якщо ви інтегруєте React у існуючий додаток, ви можете почати знизу вгору з невеликим компонентом, наприклад Button, і поступово працювати у верхній частині ієрархії перегляду.

Розбиття компонентів на частини

Не бійтеся розбивати компоненти на дрібніші компоненти.

Наприклад, розглянемо компонент Comment:

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

Спробуйте на CodePen

Він приймає author (об’єкт), text (рядок) і date (дату) як пропси і представляє собою коментар в соціальній мережі.

З цим компонентом можуть виникнути складнощі у випадку зміни вкладених елементів. Також важко повторно використовувати окремі його частини. Давайте виокремимо з нього кілька компонентів.

По-перше, створимо компонент Avatar:

function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

Компонент Avatar не повинен знати, що він рендериться всередині компонента Comment. Ось чому ми дали нашому пропсу більш загальну назву: user, а не author.

Ми рекомендуємо називати пропси з точки зору компонента, а не з контексту, в якому вони використовуються.

Тепер ми можемо спростити і зменшити Comment:

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <Avatar user={props.author} />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

Далі ми виокремимо компонент UserInfo, який відрендерить Avatar поруч з ім’ям користувача:

function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}

Це дозволить нам ще більше спростити Comment:

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

Спробуйте на CodePen

Розбиття компонентів може здатися спочатку невдячною роботою. Проте, у великих додатках така велика кількість багаторазових компонентів є дуже корисною. Суть в тому, що якщо частина вашого інтерфейсу використовується кілька разів (Button,Panel, Avatar), або сама собою досить складна (App, FeedStory,Comment), краще винести її в окремий компонент.

Пропси можна тільки читати

Незалежно від того як ви оголосите компонент як функцію чи клас, він ніколи не повинен змінювати свої власні пропси. Розглянемо функцію sum:

function sum(a, b) {
  return a + b;
}

Такі функції називаються “чистими”, оскільки вони не намагаються змінити свої аргументи і завжди повертають один і той же результат для тих же аргументів.

Для порівняння, наступна функція нечиста, оскільки вона змінює свої власні аргументи:

function withdraw(account, amount) {
  account.total -= amount;
}

React досить гнучкий, але має одне суворе правило:

Всі React-компоненти повинні працювати як чисті функції відносно їхніх пропсів.

Звичайно, інтерфейси користувачів в додатках динамічні і змінюються з часом. У наступному розділі ми представимо нову концепцію “станів”. Стан дозволяє React-компонентам змінювати їхній вивід кожного разу у відповідь на дії користувача, відповіді мережі та всього іншого, не порушуючи цього правила.