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

Nuttavut Thongjor

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

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

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

JavaScript
1class MyComponent extends Component {
2 ...
3}
4
5export default withRouter(
6 connect(mapStateToProps, mapDispatchToProps)(
7 injectIntl(MyComponent)
8 )
9)

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

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

JavaScript
1import { compose } from 'redux'
2
3class MyComponent extends Component {
4 ...
5}
6
7export default compose(
8 withRouter,
9 connect(mapStateToProps, mapDispatchToProps),
10 injectIntl
11)(MyComponent)

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

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

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

JavaScript
1class MyComponent extends Component {
2 render() {
3 return (
4 <NextComponent
5 property1={property1}
6 property2={property2}
7 property3={property3}
8 >
9 Children
10 </NextComponent>
11 )
12 }
13}

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

JavaScript
1class MyComponent extends Component {
2 render() {
3 return (
4 <NextComponent
5 property1={property1}
6 property2={property2}
7 property3={property3}
8 >
9 Children
10 </NextComponent>
11 )
12 }
13}

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

JavaScript
1const MyComponent = () => (
2 <NextComponent
3 property1={property1}
4 property2={property2}
5 property3={property3}
6 >
7 Children
8 </NextComponent>
9)

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

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

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

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

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

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

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

TypeScript
1// person จะต้องเป็น string ตลอดฟังก์ชัน
2function greeter(person: string) {
3 return 'Hello, ' + person
4}

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

JavaScript
1/* @flow */
2
3type Props = {
4 // name ต้องเป็น string และจำเป็นต้องมี
5 name: string,
6 // ? หมายถึงมีหรือไม่มีก็ได้
7 age: ?number
8}
9
10class MyComponent extends React {
11 props: Props;
12
13 // ทำให้มั่นใจว่า render ต้องคืนค่ากลับเป็น React.Element เท่านั้น
14 render(): React.Element {
15 ...
16 }
17}

4. ใช้ classnames

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

JavaScript
1import styles from './MyComponent.css'
2
3class MyComponent extends Component {
4 render() {
5 const { status } = this.props
6
7 return (
8 <article
9 className={
10 `${styles['article'] ${status === 'published' ? styles['article--published'] : '']}`
11 }
12 )
13 }
14}

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

JavaScript
1import classNames from 'classnames'
2import styles from './MyComponent.css'
3
4class MyComponent extends Component {
5 render() {
6 const { status } = this.props
7
8 return (
9 <article
10 className={
11 classNames(
12 ${styles['article'],
13 // ถ้าเป็น published จึงจะมี .article--published
14 [styles['article--published']: status === 'published'
15 )
16 }
17 )
18 }
19}

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

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

JavaScript
1// actions/Page.js
2export const loadPages = () => ({
3 [CALL_API]: {
4 endpoint: PAGES_ENDPOINT,
5 method: 'GET',
6 types: ['LOAD_PAGES_REQUEST', 'LOAD_PAGES_SUCCESS', 'LOAD_PAGES_FAILURE'],
7 },
8})
9
10// reducers/pages.js
11const initialState = []
12
13export default (state = initialState, action) => {
14 switch (action.type) {
15 case 'LOAD_PAGES_SUCCESS':
16 return action.payload
17 default:
18 return state
19 }
20}

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

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

JavaScript
1// constants/actionTypes.js
2export const LOAD_PAGES_REQUEST = 'LOAD_PAGES_REQUEST'
3export const LOAD_PAGES_SUCCESS = 'LOAD_PAGES_SUCCESS'
4export const LOAD_PAGES_FAILURE = 'LOAD_PAGES_FAILURE'

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

JavaScript
1import { PAGES_ENDPOINT } from '../constants/endpoints'
2import {
3 LOAD_PAGES_REQUEST,
4 LOAD_PAGES_SUCCESS,
5 LOAD_PAGES_FAILURE,
6} from '../constants/actionTypes'
7
8export const loadPages = () => ({
9 [CALL_API]: {
10 endpoint: PAGES_ENDPOINT,
11 method: 'GET',
12 types: [LOAD_PAGES_REQUEST, LOAD_PAGES_SUCCESS, LOAD_PAGES_FAILURE],
13 },
14})

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

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

JavaScript
1// ตัวอย่างที่ไม่ควรทำ
2class MyComponent extends Component {
3 render() {
4 return <NextComponent prop1={prop1} prop2={prop2} prop3={prop3} />
5 }
6}
7
8// แบบอย่างที่ควรปฏิบัติ
9class MyComponent extends Component {
10 render() {
11 return <NextComponent prop1={prop1} prop2={prop2} prop3={prop3} />
12 }
13}

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

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

7.1 บ๊ายบาย semicolon

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

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

JavaScript
1// ตัวอย่างที่1
2// ก่อนปรับเปลี่ยน
3const name = 'Nuttavut Thongjor'
4const obj = {
5 name: name
6}
7
8// หลังปรับเปลี่ยน
9const name = 'Nuttavut Thongjor'
10const obj = {
11 name
12}
13
14// ตัวอย่างที่2
15// ก่อนปรับเปลี่ยน
16const obj = {
17 display: function() {
18 ...
19 }
20}
21
22// หลังปรับเปลี่ยน
23const obj = {
24 display() {
25 ...
26 }
27}

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

สารบัญ

สารบัญ

  • 1. compose ช่วยชีวิต
  • 2. แค่ใส่วงเล็บชีวิตก็เปลี่ยน
  • 3. PropTypes หรือจะสู้ FlowType
  • 4. ใช้ classnames
  • 5. แยกค่าคงที่อิสระออกจากไฟล์
  • 6. โค๊ดดูดีเมื่อกดแท็บ
  • 7. DRY ทุกสรรพสิ่ง