Babel Coder

7 เคล็ดลับเปลี่ยนโค๊ด React/Redux ให้อ่านง่ายและดูดีขึ้น

beginner

เพื่่อนๆที่เขียน React อย่างเพลิดเพลิน เคยไหมที่เวลาเขียนโค๊ดช่างสนุกสนานอย่างกับกำลังเล่นเครื่องเล่นที่ดิสนีย์แลนด์ แต่พอกลับมาดูโค๊ดในภายหลังช่างฝันร้ายดั่งโดนผีหลอกในบ้านผีสิง วันนี้ผมจะนำเสนอ 7 วิธีที่จะทำให้โค๊ด React ของเพื่อนๆอ่านง่ายและดูมีสไตล์มากขึ้นครับ

1. compose ช่วยชีวิต

ลองจินตนาการถึงคอมโพแนนท์ตัวนึงที่เราเขียนขึ้น เราต้องการใช้ connect เพื่อผูก store เข้ากับ component ต้องการใช้ withRouter เพื่อสามารถเรียก this.props.router ได้จากในคอมโพแนนท์เลย และสุดท้ายต้องการเรียก injectIntl เพื่อให้สามารถทำ i18n ผ่าน this.props ได้ ด้วยเหตุนี้เราจึงเขียนคอมโพแนนท์พร้อม export ลักษณะนี้

class MyComponent extends Component {
  ...
}

export default withRouter(
  connect(mapStateToProps, mapDispatchToProps)(
    injectIntl(MyComponent)
  )
)

เมื่อเราดูโค๊ดข้างบนแล้วช่างชวนสับสนยิ่งนัก การใส่ของซ้อนวงเล็บมันทำให้เราเริ่มคิดเป็นลำดับ ปุถุชนแบบเราจะเริ่มมองของที่อยู่ในวงเล็บในสุดก่อน แล้วจึงค่อยๆถอยออกมาชั้นนอก นั่นละฮะมันช่างยากที่จะเข้าใจ

ใน Redux เรามี compose ที่ใช้ประกอบฟังก์ชันให้มีความสามารถเหมือนเอาฟังก์ชันแต่ละตัวมารวมกัน เราจึงสามารถผนวก connect, withRouter และ injectIntl เข้าด้วยกันผ่าน compose จึงทำให้รูปแบบการเขียนโค๊ดดูดีขึ้น ดังนี้

import { compose } from 'redux'

class MyComponent extends Component {
  ...
}

export default compose(
  withRouter,
  connect(mapStateToProps, mapDispatchToProps),
  injectIntl
)(MyComponent)

ดูอ่านง่ายขึ้นเยอะเลย ไม่ต้องมีวงเล็บซ้อนไปซ้อนมาหลายชั้นอีกแล้ว สำหรับเพื่อนๆคนไหนที่งงว่าเห้ย compose คืออะไร ลองอ่านเพิ่มเติมได้ในหัวข้อ composition จากบทความ พื้นฐาน funtional programming ใน JavaScript ครับ

2. แค่ใส่วงเล็บชีวิตก็เปลี่ยน

สมมติเรามีคอมโพแนนท์หน้าตาแบบนี้พร้อมประโยค return ที่คุ้นเคย

class MyComponent extends Component {
  render() {
    return <NextComponent
      property1={property1}
      property2={property2}
      property3={property3}>
      Children
    </NextComponent>
  }
}

เมื่อพุ่งการสังเกตไปที่ render จะพบว่า NextComponent นั้นแท็กเปิดและแท็กปิดไม่อยู่ในระนาบแนวตั้งเดียวกันทำให้อ่านยาก ลองจัดใหม่ดังนี้ด้วยวงเล็บเพื่อให้ดูดีขึ้น

class MyComponent extends Component {
  render() {
    return (
      <NextComponent
        property1={property1}
        property2={property2}
        property3={property3}>
        Children
      </NextComponent>
    )
  }
}

เพียงเท่านี้แท็กเปิดและปิดของ NextComponent ในประโยค return ก็จะตรงกันสวยงามแล้ว แต่ถ้าให้ดีกว่านี้ใช้ functional component ไปซะเลย

const MyComponent = () => (
  <NextComponent
    property1={property1}
    property2={property2}
    property3={property3}>
    Children
  </NextComponent>
)

เห็นไหมครับเพียงแค่มีวงเล็บ ชีวิตก็ดูดี๊ดีขึ้นมาแล้ว

3. PropTypes หรือจะสู้ FlowType

ในโลกของ React เรานิยมใส่ PropTypes เพื่อเป็นการตรวจสอบ props ของเราว่ามีชนิดข้อมูลตรงตามที่กำหนดหรือไม่ เช่น

class MyComponent extends React {
  static propTypes = {
    // this.props.name ต้องเป็น string และจำเป็นต้องระบุเข้ามาในคอมโพแนนท์
    name: PropTypes.string.isRequired,
    // this.props.age ต้องเป็น number แต่จะมีหรือไม่มีก็ได้
    age: PropTypes.number
  }
}

PropTypes มีปัญหาอยู่อย่างหนึ่งคือเราต้องการใช้มันเพื่อตรวจสอบชนิดข้อมูลของ props แค่ใน development เท่านั้น แต่บรรทัดที่2-6นั้นยังคงปรากฎอยู่แม้จะเป็นสภาพแวดล้อมแบบ production ก็ตาม แม้ใน production ตัว PropTypes จะไม่ทำงาน แต่เราก็คงไม่อยากให้โค๊ดมันไปปรากฎใช่ไหมครับ เพราะมันทำให้ไฟล์ใน production ของเราใหญ่ขึ้นโดยใช่เหตุ เราสามารถใช้ babel-plugin-transform-react-remove-prop-types เพื่อกำจัดคุณ PropTypes ออกจากโค๊ดของเราใน production build

แต่นั่นไม่ใช่ประเด็นที่เราจะกล่าวถึงในนี้ครับ PropTypes คือดีงาม มันช่วยเราหาข้อผิดพลาดเจอได้ง่ายเพราะมันตรวจสอบ props ของเราก่อน จะดีกว่าไหมถ้าเราเพิ่มความสามารถให้โค๊ดของเราอธิบายตัวเองได้

ใน TypeScript เราสามารถระบุชนิดข้อมูลได้ นั่นทำให้เรารู้ว่าตัวแปรตัวนี้จะมีค่าเป็นข้อมูลชนิดนั้นตลอดไปไม่เปลี่ยนแปลง ถ้าโค๊ดเราเผลอไปใช้ชนิดข้อมูลอื่น เราจะได้รับการแจ้งเตือนถึงสิ่งผิดพลาด ก่อนที่ข้อผิดพลาดนี้จะไปปรากฎบน production

// person จะต้องเป็น string ตลอดฟังก์ชัน
function greeter(person: string) {
    return "Hello, " + person;
}

ถ้าตอนนี้คุณใช้ ES2015 อยู่คุณสามารถใช้ Flow เพื่อเพิ่มความแกร่งในการตรวจสอบข้อมูลของคุณ ตัวอย่างการใช้ Flow เช่น

/* @flow */

type Props = {
  // name ต้องเป็น string และจำเป็นต้องมี
  name: string,
  // ? หมายถึงมีหรือไม่มีก็ได้
  age: ?number
}

class MyComponent extends React {
  props: Props;
  
  // ทำให้มั่นใจว่า render ต้องคืนค่ากลับเป็น React.Element เท่านั้น
  render(): React.Element {
    ...
  }
}

4. ใช้ classnames

ถ้าเรามีคอมโพแนนท์หนึ่งสำหรับแทน Article เราต้องการให้คอมโพแนนท์นี้มีคลาสของ CSS เป็น article เสมอ และให้มี article–published ด้วยถ้าบทความนั้นออกสู่สาธารณชนแล้ว ดังนี้

import styles from './MyComponent.css'

class MyComponent extends Component {
  render() {
    const { status } = this.props
    
    return (
      <article
        className={
          `${styles['article'] ${status === 'published' ? styles['article--published'] : '']}`
        }
    )
  }
}

เพื่อนๆจะพบว่าเพียงเราต้องการระบุคลาสแค่นี้กลับกลายเป็นเรื่องยากเมื่อต้องกลับมาอ่านโค๊ดอีกครั้ง เพื่อนๆสามารถทำให้การระบุคลาสเป็นเรื่องสนุกได้ด้วยการใช้ classnames

import classNames from 'classnames'
import styles from './MyComponent.css'

class MyComponent extends Component {
  render() {
    const { status } = this.props
    
    return (
      <article
        className={
          classNames(
            ${styles['article'],
            // ถ้าเป็น published จึงจะมี .article--published
            [styles['article--published']: status === 'published'
          )
        }
    )
  }
}

5. แยกค่าคงที่อิสระออกจากไฟล์

ถ้าเรามีค่าคงที่ แต่ค่าคงที่นั้นใช้ในหลายๆที่และไม่เกี่ยวข้องโดยตรงกับไฟล์นั้นเราควรแยกค่าคงที่นั้นออกจากไฟล์ เช่น action type ใน Redux เช่น

// actions/Page.js
export const loadPages = () => ({
  [CALL_API]: {
    endpoint: PAGES_ENDPOINT,
    method: 'GET',
    types: ['LOAD_PAGES_REQUEST', 'LOAD_PAGES_SUCCESS', 'LOAD_PAGES_FAILURE']
  }
})

// reducers/pages.js
const initialState = []

export default (state = initialState, action) => {
  switch(action.type) {
    case 'LOAD_PAGES_SUCCESS':
      return action.payload
    default:
      return state
  }
}

จากตัวอย่างข้างบนทั้ง LOAD_PAGES_REQUEST LOAD_PAGES_SUCCESS และ LOAD_PAGES_FAILURE เราใช้ค่าคงที่ทั้งสามในหลายที่ ถ้าเราสะกดผิดในซักไฟล์นึงหละ? โค๊ดของเราก็จะทำงานไม่ถูกต้อง นอกจากนี้ถ้าสมาชิกในทีมอยากเพิ่ม action ใหม่เขาก็ต้องไปนั่งเปิดดูในแต่ละไฟล์ว่า action ที่จะสร้างมีการใช้งานแล้วหรือยัง ถ้ามีจะได้เปลี่ยนไปใช้ชื่ออื่น

ด้วยเหตุนี้เราจึงควรแยกค่าคงที่ทั้งสามออกไปไว้อีกไฟล์ แล้วเรียกค่าคงที่เหล่านี้มาใช้งานในไฟล์ปลายทาง ดังนี้

// constants/actionTypes.js
export const LOAD_PAGES_REQUEST = 'LOAD_PAGES_REQUEST'
export const LOAD_PAGES_SUCCESS = 'LOAD_PAGES_SUCCESS'
export const LOAD_PAGES_FAILURE = 'LOAD_PAGES_FAILURE'

จากนั้นจึง import ค่าคงที่เหล่านี้ในแต่ละไฟล์

import { PAGES_ENDPOINT } from '../constants/endpoints'
import {
  LOAD_PAGES_REQUEST,
  LOAD_PAGES_SUCCESS,
  LOAD_PAGES_FAILURE
} from '../constants/actionTypes'

export const loadPages = () => ({
  [CALL_API]: {
    endpoint: PAGES_ENDPOINT,
    method: 'GET',
    types: [LOAD_PAGES_REQUEST, LOAD_PAGES_SUCCESS, LOAD_PAGES_FAILURE]
  }
})

6. โค๊ดดูดีเมื่อกดแท็บ

พยายามหลีกเลี่ยงการเขียนโค๊ดยาวติดกันเป็นพรืด โดยปกติเรานิยมตั้งค่าไม่ให้โค๊ดที่เราเขียนเกิน 80 ตัวอักษรต่อหนึ่งบรรทัด เมื่อโค๊ดยาวเกินไปให้ขึ้นบรรทัดใหม่ แล้วกดแท็บ ในกรณีของการเรียกคอมโพแนนท์พร้อมส่ง property เข้าไปด้วย ควรขึ้นบรรทัดใหม่ก่อนเขียนโค๊ดสำหรับการส่งค่าเหล่านั้น ดังนี้

// ตัวอย่างที่ไม่ควรทำ
class MyComponent extends Component {
  render() {
    return <NextComponent prop1={prop1} prop2={prop2} prop3={prop3} />
  }
}

// แบบอย่างที่ควรปฏิบัติ
class MyComponent extends Component {
  render() {
    return (
      <NextComponent 
        prop1={prop1} 
        prop2={prop2} 
        prop3={prop3} />
    )
  }
}

7. DRY ทุกสรรพสิ่ง

Don’t repeat yourself หรือ DRY เป็นหลักการที่แนะนำให้อย่าทำอะไรซ้ำซาก เช่น อย่าเขียนโค๊ดที่มีขั้นตอนซ้ำไปๆมาๆ ในที่นี้เราจะลดขั้นตอนของสิ่งที่ไม่จำเป็นเพื่อให้โค๊ดของเราดู DRY สะอาด สวยงาม เป็นระเบียบมากขึ้น

7.1 บ๊ายบาย semicolon

ES2015 เราไม่ต้องใส่ ; ก็ได้นะครับเพราะมันจะใส่ให้คุณอัตโนมัติในภายหลังเอง ในความคิดของผม ไร้ซึ่ง semicolon โค๊ดช่างดูสวยงามยิ่งนัก

7.2 ใช้ ES2015 เพื่อลดการทำซ้ำ

// ตัวอย่างที่1
// ก่อนปรับเปลี่ยน
const name = 'Nuttavut Thongjor'
const obj = {
  name: name
}

// หลังปรับเปลี่ยน
const name = 'Nuttavut Thongjor'
const obj = {
  name
}

// ตัวอย่างที่2
// ก่อนปรับเปลี่ยน
const obj = {
  display: function() {
    ...
  }
}

// หลังปรับเปลี่ยน
const obj = {
  display() {
    ...
  }
}

จบแล้วครับกับ 7 วิธีทำให้โค๊ด React ของเราดูดีและอ่านง่ายขึ้น เพื่อนๆสามารถนำหลักการนี้ไปประยุกต์ใช้กับการเขียน JavaScript อื่นๆได้ครับ เพื่อนๆที่มีหลักการอะไรเพิ่มเติมอยากแนะนำ อย่าเก็บไว้คนเดียวครับ ร่วมกันแบ่งปันได้ข้างล่างนี้เลยฮะ


แสดงความคิดเห็นของคุณ


Nuttavut Thongjor10 เดือนที่ผ่านมา

@saknarak ขอโทษด้วยครับ ผมตัดตอนย่อไปหน่อย

จริงๆแล้ว CALL_API เป็น Symbol ครับ เพื่อให้มัน unique และเรียกใช้ได้ตลอด application (ประกาศไว้ในไฟล์อื่นอีกที)

const CALL_API = Symbol('Call API')

ส่วนที่ต้องใส่ [] ครับ CALL_API เพื่อให้เป็น computed property key ครับ ถ้าเราไม่ใส่ [] key ของเราจะกลายเป็น string

const sym = Symbol("foo")
const obj = {sym: 1}
console.log(typeof Object.keys(obj)[0]) // string

ทำไมเราต้องมี CALL_API เป็น symbol?

  • เพื่อให้ unique ตลอด application
  • ทำให้ดูเหมือนเป็น key พิเศษคล้ายการทำ metaprogramming
*[Symbol.iterator]() {
    for(let word of this.words) {
      yield word
    }
  }

saknarak10 เดือนที่ผ่านมา

สงสัยครับ

export const loadPages = () => ({
  [CALL_API]: {
    endpoint: PAGES_ENDPOINT,
    method: 'GET',
    types: [LOAD_PAGES_REQUEST, LOAD_PAGES_SUCCESS, LOAD_PAGES_FAILURE]
  }
})

CALL_API ทำไมต้องเป็น array ครับ run แล้ว error ไม่รู้จัก CALL_API

เพิ่ม const CALL_API=‘CALL_API’ ไปถึงจะ run ได้

และผลลัพธ์เหมือนกับ CALL_API: {} เลย ลองใช้แบบนี้ดูก็ไม่ได้ [CALL_API,CALL_API2]: {} แล้วทำไมถึงต้องเป็น array ทำไมไม่เขียน CALL_API: {} แบบปกติครับ ไม่รู้จริง ๆ