Render UI in React

เนื้อหาของคอร์สนี้ เรียบเรียงขึ้นจาก React.js Fundamental
จากตอนที่แล้วใน ConfirmBattle component เรายังไม่ได้ใส่ PropTypes เลย ตอนนี้ได้เวลาเพื่อการ Refactoring แล้ว

ใส่ PropTypes ให้ ConfirmBattle

เปิดไฟล์ app/components/ConfirmBattle.js แล้วเพิ่ม code เข้าไปในตอนท้ายของ module ดังนี้

เพิ่มปุ่ม “เริ่มการต่อสู้” ให้กับ ConfirmBattle

ใน path /battle เราจะเพิ่มปุ่มเข้าไปหนึ่งปุ่ม ซึ่งเป็นปุ่มที่ใช้สั่งให้เริ่มการต่อสู้ระหว่าง github user ทั้ง playerOne และ playerTwo โดยเราจะทำการแยกระหว่าง business logic และ UI ออกจากกัน โดยให้ business logic อยู่ใน container และ UI อยู่ใน component เหมือนเดิม

ให้เปิด app/containers/ConfirmBattleContainer.js ขึ้นมา แล้วเพิ่ม function เข้าไปดังนี้

ฟังก์ชั่นตัวนี้ทำหน้าที่ในการพา user ไปยัง path ใหม่ที่ชื่อว่า /results  พร้อมทั้งระบุให้ state ของ component ใหม่ในหน้าดังกล่าวมีค่าของ playersInfo เท่ากับ this.state.playerInfo

จากนั้นก็ส่งผ่าน function นี้ให้เป็น props ของ ConfirmBattle component

และแน่นอนว่า เมื่อมีการส่ง props มาที่ component ใดก็ตาม โดย best practice แล้ว ควรจะสร้าง PropTypes ไว้รองรับ เพื่อจุดประสงค์สองประการคือ

  1. Data Type Checking
  2. Props Documentation

ให้กลับไปที่ app/components/ConfirmBattle.js แล้วเพิ่ม propType ดังนี้

จากนั้นก็ได้เวลาสร้าง UI ให้กับ ConfirmBattle เสียที

ภายใน ConfirmBattle.js ให้เพิ่ม code ในส่วนของ UI rendering ดังนี้

Code ข้างบน เป็นโครงสำหรับไว้วางปุ่ม และ component ย่อยต่างๆ ที่เรากำลังจะเตรียมต่อไป

จะเห็นว่า code ข้างต้นมีการอ้างอิงถึง styles.space ด้วย ซึ่งเป็น style ที่เรายังไม่ได้สร้าง ให้เราเปิดไฟล์ app/styles/index.js ขึ้นมาแล้วแก้ code ดังนี้

เมื่อเสร็จแล้วลองรันโปรแกรมดูครับ เมื่อไล่ไปจนถึง path /battle หากไม่มีอะไรผิดพลาดควรจะเจอรูปเดียวกับผม

screen-shot-2559-11-06-at-12-27-52-pm

โดยปุ่ม Initiate Battle! เป็นปุ่มที่กดแล้วจะไปที่หน้า /results พร้อมกับ state ของ playersInfo ส่วนปุ่ม Reselect Players คือปุ่มที่กดแล้วจะทำการเลือก github user ใหม่ โดยพาไปที่ path /playerOne

หากลองกดปุ่ม Reselect Players มันจะพาเราไป path playerOne/ ได้อย่างถูกต้อง แต่พอกดปุ่ม Initiate Battle! มันจะขึ้น error ที่ console ว่า

นั่นเพราะเรายังไม่ได้สร้าง route สำหรับ path /results เลย

เรื่อง path /results ขอติดไว้ก่อน ตอนนี้ที่ต้องทำก่อนก็คือ component ที่ใช้แสดงรายละเอียดของ github user ชื่อว่า UserDetails

UserDetails Component

ให้สร้างไฟล์ใหม่ใน app/components/ ชื่อว่า UserDetails.js (app/components/UserDetails.js) โดยมีรายละเอียดดังนี้

จุดที่อยากจะเน้นก็คือ ไวยกรณ์ของ JSX ที่ผมไฮไลท์ไว้ อย่างเช่น

ตัว && operator ใน JSX นั้นทำหน้าที่เหมือน ternary Operator ดังนั้น หากนำ code นี้มาเขียนใหม่ด้วย ternary จะได้ดังนี้

ซึ่งหมายความว่า หากไม่มี props user.score ไม่ต้อง render ตัว <li> ชุดนี้ การนำ && มาใช้จะทำให้ประโยคในการเขียนบนหน้า UI สั้นลง (แต่ก็เสี่ยงสร้างความสับสนมากขึ้นเช่นกัน)

ส่วนเครื่องหมาย !! ที่ปรากฎอยู่ข้างหน้า user.score คือ operator ที่แปลงค่าของทุกสรรพสิ่งในโลกของ JavaScript ให้มีค่าเป็น boolean นั่นเอง

อีกส่วนที่อยากเน้นย้ำก็คือ บรรทัดต่อไปนี้

สำหรับ props ที่เป็น nested props ที่ซ้อนอยู่ภายใต้ props อื่นๆ อีกชั้นหนึ่งนั้น สามารถระบุได้โดยใช้ PropTypes.shape ได้ ทำให้เราสามารถเห็นได้อย่างชัดเจนว่า component ตัวนี้รับ props อะไรมาบ้าง (นี่คือจุดประสงค์ด้าน document ของ component ซึ่งอาจจะดูน่ารำคาญ แต่เชื่อผมเถอะ สละเวลาทำ document ซักนิด ช่วยชีวิตเราในฐานะ developer ในระยะยาวได้ดีนักแล)

นำ UserDetails component มาใช้

ให้เรากลับไปที่ ConfirmBattle component เพื่อนำ UserDetails ที่เราเพิ่งสร้างเสร็จไปหยกๆ มาใช้งาน

ให้เปิดไฟล์ app/components/ConfirmBattle.js ขึ้นมา แล้วแก้ code ดังนี้

ในส่วนของ UserDetails ที่เราเรียกมาใช้งานใน ConfirmBattle component นั้น มีการส่งผ่าน info props ไปเท่านั้น ยังไม่ได้ส่ง score props ไปให้ (หากยังจำกันได้ UserDetails จะต้องรับ props สองตัว กล่าวคือ score และ info แต่ตัว score ไม่ใช่ props บังคับ เนื่องจากเราไม่ได้ใส่ isRequired ไว้ ดัง code ต่อไปนี้)

จะเห็นว่า score นั้นถูกระบุไว้ว่าเป็น PropTypes ของ UserDetails component แต่ไม่ได้ระบุ isRequired ไว้ ดังนั้นถือว่าเป็น optional prop types

คราวนี้ให้เราลองกลับไปเล่นที่ localhost:8080 อีกครั้งหนึ่ง หากไม่มีอะไรผิดพลาด ใน path /battle ทุกคนน่าจะเห็นภาพเดียวกับผม

screen-shot-2559-11-06-at-1-31-57-pm

Refactoring เพื่อให้เป็นไปตามหลัก DRY (Don’t Repeat Yourself)

หากลองกลับไปดูที่ code ใน ConfirmBattle จะเห็น code ในส่วนของ UserDetails เป็นดังนี้

สังเกตมั๊ยครับว่า ตรง code ที่ผมไฮไลท์ไว้ เป็น code ที่เขียนซ้ำกัน ซึ่งอาจจะดูเป็นเรื่องเล็กน้อย เพราะมีไม่กี่บรรทัดเท่านั้น

คราวนี้เปลี่ยน scenario ใหม่ สมมติว่า จู่ๆ มี requirement มาให้คุณต้องให้โปรแกรมนี้แสดงผล github user ได้พร้อมกัน 6 คน นี่แปลว่าคุณจะต้องทำการ copy & paste บรรทัดที่ผมไฮไลท์ไว้เพิ่มเข้าไปจนครบ 6 ชุด

อันนั้นยังไม่วิกฤติเท่าไหร่ ลองจินตนาการต่อว่า มี requirement ให้เราเปลี่ยน className เป็นตัวอื่น นั่นแปลว่าเราก็ต้องไล่แก้ที่ละ tag ไปจนครบ 6 ชุด

หากคุณผู้อ่านมองแค่ตัวอย่าง code ข้างต้น ก็คงจะรู้สึกว่ามันไม่ใช่งานหนักอะไรนักหนา คำถามของผมก็คือ แล้วหาก code ที่ duplicated แบบนี้มันซับซ้อนกว่านี้ล่ะครับ? หากมันมีปริมาณ code ที่เยอะกว่านี้ล่ะครับ?

พอจะเห็นประเด็นของผมแล้วใช่มั๊ยครับว่า การ copy & paste code เพื่อการทำซ้ำนั้นไม่สนุก และเสี่ยงต่อการสร้าง bug ใหม่

ฝรั่งเขาเลยคิด best pracetice ตัวนึงขึ้นมาเรียกว่า DRY หรือ Don’t Repeat Yourself (แปลเอาเองนะครับ 555) คือ อย่าเขียน code ซ้ำ หากมี code ตัวใดที่เข้าข่ายนี้ถือว่ามีกลิ่นไม่ดี (Code Smell ซึ่งเป็นอีกเรื่องสำคัญที่หากโชคชะตาเอื้ออำนวย ผมจะเขียนเรื่องพวกนี้ทั้งหมดเลยครับ) และจำเป็นต้องได้รับการ refactor

สำหรับตัวอย่างนี้ เราจะทำการ Refactor โดยการ extract ส่วนที่ซ้ำกันไปสร้างเป็น component ใหม่ ที่ชื่อว่า UserDetailsWrapper.js

ให้เราสร้างไฟล์ใหม่ใน app/components/ ชื่อว่า UserDetailsWrapper.js (app/components/UserDetailsWrapper.js) โดยมี code ดังนี้

ที่จะเน้นก็คือ ใน component นี้จะมีการรอรับ props.children หรือ component ที่อยู่ภายใต้ตัวมันด้วย เพราะเราจะต้องเอา component ตัวนี้ไปห่อหุ้ม UserDetails component อีกที อีกประการคือ จาก code เราจะเห็นได้ทันทีเลยว่า ตัว UserDetailsWrapper นี้มี required props เป็น header ซึ่งจะทำให้เวลาเราเรียกใช้จะได้ไม่ผิดพลาด (เริ่มเห็นข้อดีของ PropTypes ขึ้นมาบ้างหรือยังครับ)

ให้เปิดไฟล์ app/components/ConfirmBattle.js ขึ้นมาแล้วแก้ code ดังนี้

ลองกลับไปเล่นโปรแกรมดูครับ หากไม่มีอะไรผิดพลาด ทุกอย่างควรจะทำงานได้เหมือนเดิม (ตอนนี้ผมยังไม่ได้สอนการเขียน test ให้ React ซึ่งผมจะสอนในคอร์สขั้นสูงต่อไป ถึงตอนนั้นเราจะไม่ต้องไปเปิด browser เพื่อทดสอบด้วยตัวเองซ้ำๆ แบบนี้อีกแล้ว แต่ตอนนี้ให้ทำแบบนี้ไปก่อนครับ)

สำหรับคนที่อยากได้ code ต้นแบบสำหรับบทนี้ สามารถไปดูได้ที่

https://github.com/himaeng/react-fun-course/tree/master/08-userDetails

สรุป

เนื้อหาในบทนี้เน้นไปที่การสร้าง UI ให้มันสมบูรณ์ และการ Refactoring ตามหลัก DRY ครับ