Overreacted

Чому ми пишемо super(props)?

2018 M11 30 • ☕️ 5 min read

Я чув, що Hooks зараз нова гаряча тема. Іронічно, але я хочу розпочати цей блог з опису цікавих фактів про class компоненти. Як щодо цього!

Ці пастки (неочевидні речі) не є важливими для продуктивного використання React. Але ви можете знайти їх цікавими, якщо ви любите копати глибше задля розуміння як все працює.

І ось перша неочевидна річ.


За своє життя я писав super(props) більше разів ніж насправді хотів би того:

class Checkbox extends React.Component {
  constructor(props) {
    super(props);    this.state = { isOn: true };
  }
  // ...
}

Звісно, class fields proposal дозволяє уникати цього:

class Checkbox extends React.Component {
  state = { isOn: true };
  // ...
}

Синтаксис на зразок цього планувався, коли React 0.13 додав підтримку звичайних класів у 2015. Визначення constructor та виклик super(props) замислювався як тимчасове рішення, допоки класи не запровадять більш ергономічну альтернативу.

Але давайте повернемося до цього прикладу, використовуючи лише властивості ES2015:

class Checkbox extends React.Component {
  constructor(props) {
    super(props);    this.state = { isOn: true };
  }
  // ...
}

Чому ми викликаємо super? Чи можемо ми не робити цього? Якщо ми зобов’язані викликати його, що трапиться, якщо ми не передамо props? Чи є будь-які інші аргументи у super? Давайте розберемося.


У JavaScript super посилається на конструктор базового класу. (У нашому прикладі він посилається до реалізації React.Component.)

Важливо, що ви не можете використовувати this у конструкторі, допоки ви не викличете базовий конструктор. JavaScript не дозволить вам зробити це:

class Checkbox extends React.Component {
  constructor(props) {
    // 🔴 Поки не можна використовувати `this`
    super(props);
    // ✅ Тепер, мабуть, все ok
    this.state = { isOn: true };
  }
  // ...
}

Є чудова причина, чому JavaScript примушує, щоб базовий конструктор виконувався перед тим, як ви використаєте this. Роздивимось ієрархію класів:

class Person {
  constructor(name) {
    this.name = name;
  }
}

class PolitePerson extends Person {
  constructor(name) {
    this.greetColleagues(); // 🔴 Це не дозволяється, читай нижче чому
    super(name);
  }
  greetColleagues() {
    alert('Good morning folks!');
  }
}

Уявіть якщо використання this перед викликом super було б дозволено. Місяць потому, ми б змінили greenColleagues, яка включала б в себе ім’я персони у повідомленні.

  greetColleagues() {
    alert('Good morning folks!');
    alert('My name is ' + this.name + ', nice to meet you!');
  }

Але ми забули, що this.greetColleagues() викликається перед викликом super() і не має шансу встановити значення this.name. Тож this.name досі не було би визначено! Як ви бачите, поводження такого коду складно передбачити.

Для того щоб обійти подібні неочевидності, JavaScript змушує вас спочатку викликати super, якщо ви бажаете використовувати this у конструкторі. Нехай батьки роблять те, що повинні робити! І ці обмеження застосовуються до React-компонентів, які визначенні як класи:

  constructor(props) {
    super(props);
    // ✅ Тепер можливо використовувати `this`
    this.state = { isOn: true };
  }

Але залишається інше питання: навіщо передавати props?


Ви можете подумати, що передавання props до super необхідно для того, щоб базовий конструктор React.Component зміг ініціалізувати this.props:

// Усередині React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

І це дійсно недалеко від правди, ось що він робить.

Але якщо ви викличите super() без аргумента props, ви всеодно будете мати доступ до this.props у методі render та інших. (Якщо ви мені не вірите, спробуйте самі!)

Як це працює? Виявляється, що React також присвоює props екземпляру класа одразу після виклику вашого конктруктора:

  // Усередині React
  const instance = new YourComponent(props);
  instance.props = props;

Тож навіть якщо ви забудете передати props до super(), React все одно одразу їх виставить. І для цього є причина.

Коли React додав підтримку класів, він додав не лише підтримку ES6 класів. На меті була підтримка усіх абстракцій класів, яка лише можлива. Було не очевидно наскільки успішними можуть бути ClojureScript, CoffeeScript, ES6, Fable, Slaca.js, TypeScript, або інші рішення для визначення компонентів. Тож React був навмисно замисленний щодо відсутності необхідності виклику super() – навіть якщо у ES6 це обов’язково.

Отже це означає, що ви можете писати лише super() замість super(props)?

Взагалі-то, ні, адже це все ще дуже заплутано. Звісно, пізніше React присвоїть this.props після виконання вашого конструктора. Але this.props буде досі undefined між викликом super та кінцем вашого конструктора:

// Усередині React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

// Усередині вашого коду
class Button extends React.Component {
  constructor(props) {
    super(); // 😬 Ми забули передати props
    console.log(props);      // ✅ {}
    console.log(this.props); // 😬 undefined   }
  // ...
}

І це може бути те ще випробування для дебаґінгу, якщо це трапиться у якомусь методі, який викликається з конструктора. І ось чому я рекомендую завжди передавати super(props), навіть якщо це і не є суворо обов’язковим:

class Button extends React.Component {
  constructor(props) {
    super(props); // ✅ Ми передали props
    console.log(props);      // ✅ {}
    console.log(this.props); // ✅ {}
  }
  // ...
}

Це забезпечує, що значення this.props встановлено, навіть перед виходом з конструктору.


Є ще одна річ, яка для тривалих користувачів React може бути цікавою.

Ви могли відмітити, що коли ви використовуєте Context API у класах (зі старим contextTypes або з сучасним contextType API доданим у React 16.6), context передається другим аргументом до конструктора.

Тоді чому ми не пишемо super(props, context)? Ми можемо, але context використовується набагато рідше, тому ця пастка (неочевидність) не така вже і важлива.

Разом з пропозицією щодо полів класа (class fields proposal) ця пастка повністю зникає. Без явного конструктора, усі аргументи передаються донизу автоматично. Саме це дозволяє включати посилання на this.props або this.context до виразів на кшталт state = {}.

Разом з хуками (Hooks) ми навіть не маємо super або this. Але це тема іншого дня.