Overreacted

ทำไมเราถึงต้องเขียน super(props)?

2018 M11 30 • ☕️ 3 min read

ผมได้ยินมาว่า Hooks กำลังดังมาก แต่ผมจะเปิดตัวบล็อคนี้ ด้วยการอธิบายถึงความรู้สนุกๆเกี่ยวกับ component ที่สร้างจาก 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 เริ่มรองรับการใช้ class เมื่อปี 2015 การประกาศ constructor และ super(props) นั้นตั้งใจไว้ว่าจะให้เป็นเพียงทางแก้ชั่วคราวจนกว่าจะมีวิธีอื่นที่เหมาะสมในการใช้ class fields

กลับมาที่ตัวอย่างที่ใช้แค่ฟีเจอร์ของ ES2015 กันก่อน

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

ทำไมเราต้องเรียก super? แล้วไม่เรียกได้มั้ย? ถ้าจะต้องใช้มัน จะเกิดอะไรขึ้นถ้าเราไม่ส่ง props เข้าไป? มันรับตัวแปรอื่นๆอีกไหม? มาหาคำตอบกัน


ใน JavaScript, super นั้นก็คือ constructor ของคลาสแม่ (ในตัวอย่างนี้ จะหมายถึงคลาส React.Component)

สิ่งสำคัญคือ คุณจะใช้ this ไม่ได้ จนกว่า คุณจะเรียก constructor ของคลาสแม่ก่อน JavaScript จะไม่ยอมให้คุณทำแบบข้างล่างนี้:

class Checkbox extends React.Component {
  constructor(props) {
    // 🔴 ยังใช้ `this` ไม่ได้
    super(props);
    // ✅ แต่ตอนนี้ได้ละ
    this.state = { isOn: true };
  }
  // ...
}

มันมีเหตุผลอยู่ว่าทำไม JavaScript ถึงบังคับให้เรียก constructor ของคลาสแม่ก่อนที่จะแตะต้อง 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 ได้ หนึ่งเดือนให้หลัง เราอาจจะอยากเปลี่ยน greetColleagues ให้ใส่ name ของคลาส Person เข้าไปในข้อความ:

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

แต่เราลืมไปว่า this.greetColleagues() ถูกเรียกก่อนที่ super() จะมีโอกาสได้ตั้งค่าให้กับ this.name ดังนั้น this.name จึงยังเป็น undefined อยู่! เห็นไหมว่ามันยากมากนะที่จะนึกถึงถ้าเขียนโค้ดแบบนี้

เพื่อที่จะเลี่ยงหลุมพรางข้างต้น JavaScript เลยต้องบังคับว่าถ้าคุณอยากใช้ this ใน constructor คุณ ต้อง เรียก super ก่อน เพื่อให้คลาสแม่ทำหน้าที่ของมัน! และข้อจำกัดนี้ก็ถูกนำไปใช้กับ React component ที่สร้างจาก class ด้วย:

  constructor(props) {
    super(props);
    // ✅ ใช้ `this` ได้ละ
    this.state = { isOn: true };
  }

แล้วก็มีอีกคำถามเกิดขึ้นคือ: ทำไมต้องส่ง props เข้าไปล่ะ?


คุณอาจจะคิดว่า เราส่ง props ผ่าน super เพราะ constructor ของ React.Component จำเป็นต้องเอา props ไปตั้งค่าให้กับ this.props

// ข้างในของ React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

ก็เกือบจะถูกต้องเลยนะ — จริงๆแล้วนั่นแหละคือ สิ่งที่มันทำ

แต่อย่างไรก็ตาม แม้ว่าคุณจะเรียก super() โดยไม่มีตัวแปร props คุณก็จะยังสามารถที่จะเข้าถึง this.props ใน render หรือฟังชันอื่นๆได้ (ถ้าไม่เชื่อก็ไปลองดูได้)

มันเป็นไปได้ยังไง? จริงๆแล้ว React นั้นมีการแทนค่า props ไว้ให้ตั้งแต่ตอนเรียก constructor ของคุณแล้ว:

  // ข้างในของ React
  const instance = new YourComponent(props);
  instance.props = props;

ดังนั้นถึงแม้ว่าคุณจะลืมส่ง props เข้าไปใน super() React ก็ยังจะแทนค่าให้มันอยู่ดี ซึ่งมันก็มีเหตุผลอยู่ว่า

ตอน React เริ่มรองรับการใช้ class มันไม่ได้แค่รองรับแค่คลาสของ ES6 เท่านั้น เป้าหมายคือมันต้องรองรับวิธีการสร้าง class ให้มากแบบที่สุดเท่าที่จะทำได้ เพราะมันไม่มีความชัดเจน เลยว่า ClojureScript, CoffeeScript, ES6, Fable, Scala.js, TypeScript หรือภาษาอื่น ๆ จะมีวิธีสร้าง component กันยังไง ดังนั้น React ก็เลยตั้งใจที่จะไม่ยึดติดกับการบังคับให้เรียก super() แม้ว่ามันจะจำเป็นถ้าคุณจะใช้คลาสจาก ES6

แสดงว่า ถ้าเขียนแค่ super() แทน super(props) ก็ได้สิ?

ไม่น่าจะได้ เพราะมันยังมีความงงอยู่ ถึงแม้ว่า React จะตั้งค่า this.props ให้หลังจากเรียก constructor ของคุณไปแล้ว แต่ this.props จะยังเป็น undefined ในระหว่างที่ กำลังเรียก super และ constructor ของคุณ:

// ข้างใน 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  }
  // ...
}

มันจะยิ่ง debug ยาก ถ้าอะไรแบบนี้เกิดขึ้นในสักฟังชันที่ถูกเรียกใน constructor อีกที และนั่นคือเหตุผลว่าทำไมผมถึงแนะนำให้ใช้ super(props) เสมอ ถึงแม้ว่ามันจะไม่ได้จำเป็นอย่างยิ่งก็เถอะ

class Button extends React.Component {
  constructor(props) {
    super(props); // ✅ We passed props
    console.log(props);      // ✅ {}
    console.log(this.props); // ✅ {}
  }
  // ...
}

นี้จะทำให้มั่นใจได้ว่า this.props จะถูกตั้งค่าแน่นอนตั้งแต่ก่อนที่จะจบการทำงานของ constructor


มีข้อสุดท้ายที่คนใช้ React มานานอาจจะเคยสงสัย

คุณอาจจะเคยสังเกตว่าตอนที่คุณใช้ context ในคลาส (ทั้ง contextTypes แบบเก่าหรือแบบใหม่ที่เพิ่งเพิ่มเข้ามาใน React 16.6) context จะถูกส่งเข้ามาใน constructor เป็นตัวแปรที่สอง

แล้วทำไมเราถึงไม่เขียน super(props, context) แทนล่ะ? ก็ทำได้นะ แต่ context มันถูกใช้ไม่บ่อยนัก ดังนั้นปัญหาแบบข้างบนก็เลยเกิดขึ้นไม่เยอะเท่าไหร่

หลุมพรางแบบนี้จะหายไปถ้าใช้ class fields proposal การไม่ใช้ constructor ทำให้ตัวแปรทุกตัวถูกส่งผ่านลงไปโดยอัตโนมัติ และช่วยให้การประกาศ state = {} นั้นอ้างไปถึง this.props หรือ this.context ได้ด้วยถ้าจำเป็น

super หรือ this นั้นจะไม่ต้องมีเลยนะถ้าใช้ Hooks แต่เราไว้คุยเรื่องนั้นกันวันหลัง