React มือใหม่ part 2: Class-style components
Class-style components เป็นวิธีเขียน component ดั่งเดิมของ React ข้อมูลที่ส่งเข้า component จะถูกจัดการด้วย props และการเปลี่ยนแปลงข้อมูลภายใน component จะถูกจัดการด้วย state
เนื้อหาก่อนหน้านี้
มือใหม่ได้เห็น Home และ About components มาแล้ว ทั้งสองเป็น Function components แต่ part นี้ผมอยากเล่าวิธีการเขียน component แบบดั่งเดิมของ React ที่เรียกว่า Class-style components ก่อนครับ
เนื้อหาที่อยากเล่าใน part นี้ประกอบด้วย
- การสร้างโปรเจกต์ใหม่ และเขียน Class-style component
- ออบเจ็กต์ props
- ออบเจ็กต์ state
- props vs state
Class-style components
สร้างโปรเจกต์ใหม่กันครับ
create-react-app class-style-components --template typescript
ถัดจากนั้นสร้าง src/Hello.tsx เนื้อหาดังนี้
import React from 'react'export default class Hello extends React.Component {
render() {
return <div>I am Hello</div>
}
}
เปิดไฟล์ src/App.tsx เพิ่มการเรียก <Hello/>
import React from 'react';
import './App.css';
import Hello from './Hello'function App() {
return (
<div className="App">
<header className="App-header">
<Hello/>
</header>
</div>
);
}export default App;
รัน npm start
ที่ http://localhost:3000
ตัวอย่างข้างต้นเราได้สร้างคลาสชื่อ Hello สืบทอดความเป็น component จาก React.Component
เมื่อมีความเป็น component แล้วก็สามารถ override ฟังก์ชัน render ได้ จากนั้นเขียน JSX ที่ต้องการออกไปผ่านคำสั่ง return
การสร้าง component ลักษณะนี้เรียกว่า Class-style components
export default
เขียนเพื่อบอกว่าคลาสนี้เป็น main export สำหรับ module นี้ นัยสำคัญคือเมื่อ import คลาสนี้ไปใช้ไม่ต้องเขียนมันในเครื่องหมาย { }
ออบเจ็กต์ props
เมื่ออยากส่งค่าให้กับ <Hello/>
หรือ component ใดๆ React บอกว่าให้ส่งผ่านออบเจ็กต์ props
แก้ไข src/App.tsx
<div className="App">
<header className="App-header">
<Hello myName="I am Pros"/>
</header>
</div>
ออบเจ็กต์ props ในที่นี้ก็คือ JavaScript object ธรรมดา เช่น {}
จากตัวอย่างผมนิยามให้ ออบเจ็ตก์ props ประกอบด้วย property ชื่อ myName ดังนั้นนี่คือหน้าตาของมัน
{ myName: string }
แก้ไข src/Hello.tsx เพิ่ม constructor รับตัวแปร props เข้ามา (ต้องชื่อนี้เท่านั้น)
import React from 'react'export default class Hello
extends React.Component<{ myName: string }> {
constructor(props: { myName: string }) {
super(props)
}render() {
return <div>Hello, {this.props.myName}</div>
}
}
และเพื่อให้โค้ดนี้ดูง่ายจะนำ { myName: string }
ไปเขียนเป็น interface แทน
import React from 'react'interface HelloProps {
myName?: string
}export default class Hello
extends React.Component<HelloProps> {constructor(props: HelloProps) {
super(props)
}render() {
return <div>Hello, {this.props.myName}</div>
}
}
ผลลัพธ์
HelloProps
interface มี myName เป็นสมาชิก เครื่องหมาย ? กำกับว่า property นี้เป็น optional จะกำหนดค่าหรือไม่ก็ได้ หากไม่ให้ค่าเท่ากับค่านั้นคือ undefined
ทดลองแก้ <Hello myName="I am Pros"/>
เป็น <Hello/>
เหมือนเดิม
ผลลัพธ์
เมื่อใดก็ตามที่ component สามารถมี props, React กำหนดให้แจ้งแก่มันผ่าน React.Component โดยใช้คีย์เวิร์ด super และการจะใช้คีย์เวิร์ดนี้ได้จะต้องเขียนมันไว้ภายใน constructor เท่านั้น การกระทำนี้ React สามารถทราบได้ว่า component ใดจะมี props เพื่อเตรียมพร้อมวิธีการเปลี่ยนแปลง UI เมื่อ properties ใน props เกิดการเปลี่ยนแปลงค่า
ภายใน constructor ไม่ต้องอ้างอิงคำสั่ง
this.props
ต่างจากส่วนอื่นๆต้องอ้างอิงthis.props
เสมอ
เพื่อให้เวลาไม่กำหนดค่าแก่ myName จะได้แสดงผลเป็น Hello, I am Hello. เราอาจต้องเพิ่มเงื่อนไข (if) สักเล็กน้อย
แก้ไข src/Hello.tsx
render() {
if (!this.props.myName) {
return <div>I am Hello.</div>
}
return <div>Hello, {this.props.myName}</div>
}
อย่าลืมว่าเราได้เรียก <Hello/>
เฉยๆ
ผลลัพธ์
ออบเจ็กต์ state
ข้อมูลหรือค่าใดที่เกิดการเปลี่ยนแปลงได้ใน component, React ต้องการให้จัดการด้วย state
หลักการนี้สามารถแบ่ง component เป็น 2 ประเภท คือ
- Stateful component ง่ายๆว่าใช้ state ด้วย
- Stateless component ง่ายๆว่าไม่ใช้ state เลย
components ใดที่ใช้แค่ props เพียงอย่างเดียวจะถูกเรียกว่า Stateless component เป็น component โง่ๆที่ถูกเรียกว่า dumb-as-f*ck components
components ใดที่ใช้ทั้ง props และ state จะถูกเรียกว่า Stateful component
โค้ดต่อไปนี้จะเพิ่มการ implement state แต่ผลลัพธ์จะต้องคงเดิม
Stateful component
components ใดที่ใช้ทั้ง props และ state จะถูกเรียกว่า Stateful component เป็นเช่นนั้นก็เพราะว่า state สามารถเปลี่ยนแปลงค่าของข้อมูลภายใน component ซึ่งจะแสดงให้เห็นในภายหลัง
แก้ไข src/Hello.tsx เพิ่มการกำหนดค่าให้กับ state
import React from 'react'export default class Hello
extends React.Component<HelloProps, { name: string }> {constructor(props: HelloProps) {
super(props)
this.state = {
name: `Hello, ${props.myName}`
}
}render() {
if (!this.props.myName) {
return <div>I am Hello.</div>
}
return <div>{this.state.name}</div>
}
}
และเพื่อให้โค้ดนี้ดูง่ายจะนำ { name: string }
ไปเขียนเป็น interface แทน
import React from 'react'interface HelloProps {
myName?: string
}interface HelloState {
name: string
}export default class Hello
extends React.Component<HelloProps, HelloState> {constructor(props: HelloProps) {
super(props)
this.state = {
name: `Hello, ${props.myName}`
}
}render() {
if (!this.props.myName) {
return <div>I am Hello.</div>
}
return <div>{this.state.name}</div>
}
}
ผลลัพธ์ของ <Hello myName="I am a king of Python."/>
ผลลัพธ์ของ <Hello/>
ตัวอย่างข้างต้นได้อ่านค่า myName ของ props แล้วกำหนดเป็นค่าแก่ name ของ state ก่อนจะนำไป render
props vs state
ความเหมือน
- ทั้งคู่เป็น plain JavaScript objects เขียนเป็น empty object ได้เหมือนกันคือ { }
- ทั้งคู่มีผลให้เกิดการ re-render หรือเรียกว่า render update
- เมื่อให้ค่าแก่ props และ state เหมือนกัน ทั้งคู่ย่อมให้ผลลัพธ์เดียวกัน
ความแตกต่าง
- หากให้ฟังก์ชัน sum(x, y) คืนค่าผลลัพธ์ของ x + y แล้ว props คือ x กับ y
- หากให้ฟังก์ชัน sum(x, y) คืนค่าผลลัพธ์ของ z = x + y แล้ว state คือ z
- props เป็น optional ไม่ประกาศแก่ component ได้ แต่ state เป็นออบเจ็กต์ที่มีอยู่แล้วใน React.Component
- props เป็น immutable หมายถึง read only ไม่สามารถเปลี่ยนแปลงค่าได้
- state เป็น mutable สามารถเปลี่ยนแปลงค่าได้
- แต่ละ component จะบริหารจัดการ state ของตนเอง (private) ในขณะที่แต่ละ component ในลำดับชั้น parent & child สามารถรับค่า props เดียวกันทั้งหมด (public) หรือที่เรียกว่า pass down callback functions by props
- state เปลี่ยนค่าโดยฟังก์ชัน setState ซึ่งเป็น asynchronous ในขณะที่ props เป็นแค่ read only
ตารางต่อไปนี้แสดงถึงความแตกต่างระหว่าง props กับ state
ออกแบบ component เป็น stateless หรือ stateful?
ควรออกแบบให้เป็น stateless component ให้มากที่สุด เหตุผลคือ
- ค่าของข้อมูลไม่เปลี่ยนแปลง ไม่ต้องคิดว่าอะไรจะเกิดขึ้นได้อีกนอกจากในฟังก์ชัน render
- business ทั้งหมดที่เกิดขึ้นจะไม่หนีไปจาก props ทำให้จำกัดขอบเขตได้ง่าย
- setState เป็น asynchronous การเปลี่ยนแปลงที่เกิดไม่มีลำดับแน่นอน ไม่เกิดผลในทันทีทันใดจึงยากต่อการคาดเดา (จะเห็นผลก็ต่อเมื่อโปรเจกต์โตขึ้นและซับซ้อนมากขึ้น)
แต่ก็ไม่ได้หมายความว่า stateless component เป็นทุกคำตอบของ business
ผ่านค่าเป็นออบเจ็กต์หรือฟังก์ชันให้กับ setState?
คำตอบคือฟังก์ชัน ขออธิบายด้วยการสร้างไฟล์ชื่อ AsyncCounter
สร้างไฟล์ src/AsyncCounter.tsx เพิ่มปุ่มและ action การกดปุ่มให้เรียกฟังก์ชันชื่อ handleSomething
import React from 'react'export default class AsyncCounter
extends React.Component<{}, { count: number }> {
constructor(props: {}) {
super(props)
this.state = { count: 0 }this.handleSomething = this.handleSomething.bind(this);
}handleSomething() {
this.incrementCount()
this.incrementCount()
this.incrementCount()
}incrementCount() { }render() {
return <div>
<button onClick={this.handleSomething}>Increment Count</button>
<div>count: {this.state.count}</div>
</div>
}
}
แก้ไขไฟล์ src/App.tsx
function App() {
return (
<div className="App">
<header className="App-header">
<AsyncCounter />
</header>
</div>
);
}
ผลลัพธ์
กดปุ่ม Increment Count แล้วสังเกตผลลัพธ์
ผลลัพธ์เท่ากับศูนย์ตามเดิมเพราะไม่มีการคำนวณ business ใดในฟังก์ชัน incrementCount (ถูกแกล้งซะแล้วสินะ)
แก้ไขไฟล์ src/AsyncCounter.tsx เพิ่มการคำนวณ business โดยบวกเพิ่มค่า count จากออบเจ็กต์ state แล้วโยนใส่ฟังก์ชัน setState
incrementCount() {
this.setState({ count: this.state.count + 1 })
}
ตาม business แล้วฟังก์ชัน handleSomething เรียก incrementCount ทั้งสิ้น 3 ครั้ง ดังนั้นศูนย์บวกหนึ่งถึงสามครั้งต้องมีค่าเท่ากับสามในการคลิ๊กแค่ครั้งเดียวถูกต้องหรือไม่?
ลองคลิ๊กเพียงหนึ่งครั้ง
ผลลัพธ์
แก้ไขโดยเปลี่ยนการบวกเพิ่มค่า count จากออบเจ็กต์ state เป็นการใช้ฟังก์ชันแทน
incrementCount() {
this.setState((state) => {
return { count: state.count + 1 }
});
}
ลองคลิ๊กเพียงหนึ่งครั้ง
ผลลัพธ์
อ่านเพิ่มเติม ที่นี่
มาถึงตรงนี้เพื่อนๆมือใหม่หลายคงเกาหัวไม่คิดฝันว่าแค่อยากเขียนเว็บสมัยใหม่จะต้องมารู้จัก props กับ state อะไรมากมาย ส่วนตัวก็ยอมรับว่า React มันเป็นแบบนี้และเป็นแบบนี้ตลอดเรื่องราวของมัน พื้นฐานของมันคือการจัดการหน่วยความจำที่ต้องการให้เกิดการ change เฉพาะจุดที่เกิดการเปลี่ยนแปลงเท่านั้นซึ่งเป็นข้อดีที่ทำให้ว้าวและก็เป็นข้อเสียที่ทำให้การเรียนรู้ตลอดจนการทำความเข้าใจต้องใช้เวลาพอสมควร ดังนั้นผมจึงอยากให้อดทนและเชื่อมั่นว่าเราทำได้ การที่ผมหยิบเรื่องนี้มาเล่าแต่เนิ่นๆก็เพื่อจะบอกว่าเตรียมตัวปวดสมองไปพร้อมกันนะครับ