React Life Cycle

เนื้อหาของคอร์สนี้ เรียบเรียงขึ้นจาก React.js Fundamental
ยังจำกันได้มั๊ยว่า การเขียน render() ฟังก์ชั่นใน component นั้น ควรจะเขียนในแบบ pure function ซึ่งจะไม่มีการเปลี่ยน state และสร้าง side effect ต่อภายนอกใดๆ ทั้งสิ้น อย่างเช่น การ call extenal service แบบ Ajax request, Firebase calling เป็นต้น เพราะหน้าที่ของ render() มีแค่การ render UI เท่านั้น

คำถามคือ หากเราไม่ทำสิ่งดังกล่าวภายใน render() แล้ว เราจะเอากิจกรรมเหล่านั้น (ซึ่งเป็นสิ่งจำเป็นสำหรับการพัฒนา application อย่างมาก) ไปทำที่ไหน? คำตอบก็อยู่ใน life cycle ของ React นั่นเอง

life cycle ของ React นั้นแสดงออกมาในรูปแบบของ function หลายๆ ตัว ซึ่งจะเกิดตามลำดับวงจรชีวิตของ component ของ React เช่น จังหวะที่ component เพิ่งได้รับการ render, จังหวะที่ component เพิ่งได้รับข้อมูลใหม่มา เป็นต้น

ซึ่งเราสามารถแบ่ง Life Cycle ของ React ออกได้เป็นสองกลุ่มใหญ่ๆ กล่าวคือ

  1. กลุ่มที่เกิดขึ้นในจังหวะที่ component เกิดการ mount หรือ unmount ไปยัง DOM
  2. กลุ่มที่เกิดขึ้นในจังหวะที่ component ได้รับข้อมูล

กลุ่ม Mount/Unmount

นี่คือกลุ่ม Life Cycle ที่จะเกิดขึ้นเมื่อ component ถูกแทรกเข้าไปใน DOM หรือที่เรียกว่า mount รวมทั้งในจังหวะที่ component ถูกกำจัดออกจาก DOM หรือที่เรียกว่า unmount โดย function ในกลุ่มนี้จะเกิดขึ้นเพียงครั้งเดียวต่ออายุขัยของแต่ละ component

ก่อนจะไปถึงเนื้อหาในส่วนถัดไป ผมอยากให้คุณลองนั่งนิ่งๆ แล้วลองตรึกดูซักนิดว่า ในจังหวะที่เกิด Life Cycle เหล่านี้ขึ้นมา คุณสามารถทำอะไรกับ component ได้บ้าง

รายการต่อไปนี้คือสิ่งที่เราสามารถลงมือทำได้ ในจังหวะที่เกิด Life Cycle เหล่านี้

  • Assign ค่า default ให้กับ props ต่างๆ ของ component นั้นๆ
  • เซตค่าตั้งต้นให้กับ state ต่างๆ ภายใน component นั้นๆ
  • ทำการดึงข้อมูลจากระยะไกลด้วยการทำ Ajax request เพื่อนำมาใช้งานภายใน component นั้นๆ
  • set up ตัว listener ต่างๆ (อย่างเช่น Websockets หรือ Firebase listeners)
  • ปิด listener ที่เปิดค้างไว้ (ทำในจังหวะ unmount)

หมายเหตุ: หลักใหญ่ใจความสำคัญของเนื้อหาในส่วนนี้ก็คือ การให้คุณตรึกตรองถึงแนวทางแก้ปัญหาที่คุณสามารถทำได้ใน life cycle เหล่านี้ หากคุณทำความเข้าใจ และจดจำได้ทั้งหมดแล้ว ก็อ่านเนื้อหาในส่วนถัดไปได้เลยแต่หากยังไม่เข้าใจ ได้โปรดกลับไปอ่านเนื้อหาข้างต้นอีกครั้งหนึ่ง เพราะส่วนที่เป็นหลักคิดเบื้องหลังการออกแบบเฟรมเวิร์คเหล่านี้ ไม่สามารถทำความเข้าใจผ่านการอ่าน code ได้

เรามาไล่ดูรายละเอียดของกิจกรรมเหล่านี้ทีละตัวเลยดีกว่า

Assign ค่า default ให้กับ props ต่างๆ ของ component นั้นๆ

สมมติว่า container ของ component ที่เรากำลังให้ความสนใจอยู่นั้น ลืมส่งข้อมูลผ่าน props บางตัวมาให้ แล้วเราต้องการออกแบบ component ให้มีการเซต default value ให้กับ props ดังกล่าว

วิธีการก็คือ เราต้องใช้ฟังก์ชั่น getDefaultProps ดังนี้

นั่นเท่ากับว่า component ตัวไหนก็ตามที่นำ props.text ไปใช้ แต่ดันไม่ส่งข้อมูลดังกล่าวเข้ามา ตัว props.text ก็จะมี default value เป็น ‘Loading’ เตรียมไว้อยู่แล้ว

เซตค่าตั้งต้นให้กับ state ต่างๆ ภายใน component นั้นๆ

เราเคยเห็นเคสนี้กันไปแล้วใน tutorial ก่อนหน้านี้ นั่นคือ เคสที่เราต้องการให้ component สร้าง state ขึ้นมาพร้อมกับ initial value ให้กับมันด้วย ซึ่งเราสามารถทำได้ผ่านฟังก์ชั่น getInitialState

จากตัวอย่างข้างต้น เป็นการใช้ getInitialState ในการเซตค่าให้กับ state ที่ชื่อว่า email และ password และหากยังจำกันได้ เมื่อใดก็ตามที่เราต้องการจะเซตค่าของ state เหล่านี้ในภายหลัง ให้ใช้ฟังก์ชั่น this.setState() ทำหน้าที่นี้ไป

ทำการดึงข้อมูลจากระยะไกลด้วยการทำ Ajax request เพื่อนำมาใช้งานภายใน component นั้นๆ

ขึ้นชื่อว่า Application ยังไงก็ต้องมีเรื่องที่ต้องเกี่ยวข้องกับ data และเมื่อใดก็ตามที่ component ของเราต้องมีการดึงข้อมูลโดยใช้วิธี Ajax request เราก็จะทำสิ่งเหล่านี้ผ่านฟังก์ชั่นที่ชื่อว่า componentDidMount ซึ่งเป็นฟังก์ชั่นที่จะถูกเรียกใช้เมื่อ component ถูก mount หรือแทรกเข้าไปที่ DOM เรียบร้อยแล้ว

code ข้างต้น เป็นการใช้ Axios module (module สำหรับทำ Ajax request) เพื่อดึงข้อมูลลงมา แล้วจากนั้นใช้งานฟังชั่นของ this.props.callback ในการประมวลผลข้อมูลที่ถูกดึงลงมา

หากเห็นฟังก์ชั่นแปลกๆ ที่เรียกว่า .then ก็ไม่ต้องแปลกใจ ทาง JavaScript เขาเรียกมันว่า promise เป็นการเรียกใช้ฟังก์ชั่นแบบ asynchornous ซึ่งผมจะอธิบายในเนื้อหาหลังจากนี้อีกที

Set up ตัว listeners ต่างๆ (อย่างเช่น Websockets หรือ Firebase listeners)

กรณีนี้ก็ไม่ต่างจากการเรียกใช้ Ajax request เพราะการตั้ง listener ก็เป็นการเรียกใช้ remote service เหมือนกัน ดังนั้น ในที่นี้เราจะใช้ฟังก์ชั่น componentDidMount

ปิด listeners ต่างๆ ที่เปิดค้างไว้

เมื่อใดก็ตาม ที่เราได้เซต listener สำหรับ Firebase เอาไว้ เราจะต้องทำการปิดมันทุกครั้ง โดยเราจะปิดในจังหวะที่ component ถูกกำจัดออกจาก DOM เพื่อป้องกันอาการ memory leaks

งานนี้เราจะใช้ฟังก์ชั่น componentWillUnmount

 

กลุ่ม Life Cycle ที่เกิดขึ้นเมื่อ component ได้รับข้อมูล

ฟังก์ชั่นที่เป็นพระเอกสำหรับ Life Cycle ในกลุ่มนี้ก็คือ componentWillReceiveProps ซึ่งจะเป็นฟังก์ชั่นที่ถูกเรียกเมื่อ component นั้นๆ เพิ่งได้รับค่า props มาใหม่ๆ

ส่วนฟังก์ชั่นที่เด่นในลำดับต่อมาก็คือ shouldComponentUpdate ซี่งจะมีความ advance กว่าฟังก์ชั่น componentWillReceiveProps

shouldComponentUpdate จะทำหน้าที่คอยตัดสินว่าควรจะทำการ render component หรือไม่ โดยมันจะ return ค่าออกมาเป็น boolean หากมัน retrun true ก็จะทำการ re-render แต่หากเป็น false ก็จะไม่ทำ ซึ่งเรื่องนี้จะทำให้ performance ของหน้า UI ดีขึ้นอย่างเห็นได้ชัด

ต่อไปนี้คือแผนภาพของ Life Cycle ทั้งหมดภายใน React

rxzidtc7s5weick3finw_screen-shot-2016-02-25-at-12-06-29-pm

ทดลองเล่นกับ Life Cycle ด้วยโปรเจ็กท์ทดลอง

หากเอาแต่อธิบายทฤษฎี คงจะไม่มันส์เท่าไหร่ ดังนั้น เนื้อหาในส่วนนี้ เรามาลองสร้างโปรเจ็กท์ React ตัวเล็กๆ ขึ้นมาซักตัวหนึ่ง เพื่อลองเล่นกับ Life Cycle ทั้งหมดของ React Component กันดู

สร้าง toy project ด้วย create-react-app

Facebook ได้คลอด react builder ตัวนึงขึ้นมา ซึ่งใช้งานได้ง่าย และเหมาะที่นำมาทดลองสร้างโปรเจ็กท์ React แบบ toy project (แต่ไม่แนะนำให้ใช้กับ production grade application) ชื่อว่า create-react-app

ให้เราทำการติดตั้ง create-react-app แล้วสร้างโปรเจ็กท์ตัวใหม่ขึ้นมา โดยพิมพ์ command ลงไปใน terminal ดังนี้

จากนั้นให้เปิดไฟล์ life-cycle-toy/src/index.js แล้วแก้ code ให้เป็นดังนี้

อ้างอิง: https://gist.github.com/fay-jai/fc8a5093c0b5124d4b2d#file-react-lifecycle-parent-child-jsx

ให้เปิด browser แล้วไปที่ localhost:3000 (ในกรณีที่มันไม่เปิดให้โดยอัตโนมัติ) จากนั้นให้เปิด console ดู จะเห็นข้อมูลดังภาพต่อไปนี้

พอจะเห็นภาพมั๊ยครับว่า เวลามี component สองตัวที่กำลังถูก render โดยตัวนึงเป็นแม่ อีกตัวเป็นลูก ให้คุณผู้อ่านลองศึกษาลำดับการเกิดขึ้นของ life cycle ว่ามันมีลำดับเป็นเช่นไร (ให้สังเกตุตัว child compnent ว่าเกิดขึ้นจังหวะไหน โดยผม highlight เอาให้ดูได้ง่าย)

คราวนี้เรา ลองมาดูว่า เมื่อมีการส่งข้อมูลไปยัง child component ผ่านตัวแม่ จะเกิดอะไรขึ้น

ให้เราลองพิมพ์อะไรก็ได้ลงไปใน text input บนหน้าจอ แล้วดูตรง console จะเห็น log ขึ้นมาดังนี้

ตัว log ค่อนข้างอธิบายชัดอยู่แล้วค่อนข้างมาก อยากให้คุณผู้อ่านลองศึกษา log เปรียบเทียบกับ code ใน index.js ดู เพียงแค่นี้ คุณก็จะเข้าใจถึง concept ขั้นสูงของ React Life Cycle แทบทั้งหมดแล้ว (ซึ่งน่าจะครอบคลุมการใช้งานของคุณในอนาคตแทบจะ 100% อย่างแน่นอน)

หมายเหตุ: อย่าลืมปิด server ของโปรเจ็กท์นี้ด้วยนะครับ มันเปลืองพอร์ต 5555

กลับมาหา project ของเรากันเถอะ

ได้เวลาเอาความรู้เรื่อง life cycle มาใช้งานกับโปรเจ็กท์หลักของเรากันแล้ว

จากเนื้อหาในตอนที่แล้ว หากยังจำกันได้ เรายังขาด path สุดท้ายไปอีกหนึ่งตัว นั่นคือ path /battle ที่ใช้สำหรับแสดงผลลัพธ์การแข่งขันระหว่าง github ของ player one และ player two นั่นเอง

อย่างที่เราได้ตกลงกันไว้ว่า เวลาจะสร้าง component ขึ้นมาซักตัว เราจะสร้าง container เพื่อจัดการเรื่องของ business แยกต่างหากจากงาน render UI ซึ่งเป็นหน้าที่ของ component

ดังนั้น เราจะสร้าง container ขึ้นมาก่อน โดยสร้างไฟล์ชื่อว่า app/containers/ConfirmBattleContainer.js

code ก็ไม่มีอะไรพิเศษมากครับ เพียงแค่มีการเรียกใช้ component ConfirmBattle แล้ว assign props สองตัวคือ isLoading และ playersInfo ตามลำดับ โดยนำ state ของ container ที่ชื่อเดียวกันทั้งสองตัว (this.state.isLoading และ this.state.playerInfo) มา assign ให้กับมัน ซึ่งทาง container ได้ทำการ initiate value ให้กับ state ทั้งสองตัวนี้ผ่าน getInitialState ฟังก์ชั่น

จุดที่ผมอยากจะเน้นมากๆ ก็คือ ภายในฟังก์ชั่น componentDidMount (Life Cycle ที่เกิดขึ้นตอนที่ component ถูกแทรกเข้าไปใน DOM) จะต้องมีการดึงค่า query string จาก url ออกมา ดังนั้น ผมเลยใส่ console.log ดักไว้เพื่อดูว่า ค่า props ของ container (component ตัวแม่) ตัวนี้จะมีอะไรให้เราดึงค่าดังกล่าวได้บ้าง

อีกจุดหนึ่งก็คือ ภายในตัว container นี้ เราจะมีการดึงข้อมูลจาก remote service ของ github มาใช้ ดังนั้นเราจึงต้องทำการเตรียม context ของ react-router ไว้รอท่าเพื่อการนี้ก่อน

จากนั้นให้เราสร้าง component เพื่อทำหน้าที่ render UI ขึ้นมา โดยให้ชื่อว่า app/components/ConfirmBattle.js โดยให้ใส่ code ดังนี้

ตรงนี้จะเห็นว่า เราใช้วิธีเขียน component แบบ Stateless Functional ซึ่งตัว code ก็ไม่มีอะไรสำคัญที่จะต้องอธิบาย

ขั้นสุดท้าย ให้ไปเพิ่ม path ให้กับ router ภายในไฟล์ app/config/routes.js โดยเพิ่ม code ดังนี้

จากนั้นก็ให้ลองเข้าไปใช้งานที่ localhost:8080 โดยให้เปิด console บน browser ไว้ด้วย ให้ลองกรอกข้อมูลแล้วคลิกไปจนถึงหน้าสุดท้าย เพื่อดูข้อมูลใน log จะพบข้อมูลดังนี้

screen-shot-2559-11-04-at-11-47-57-pm

จะเห็นว่าหากต้องการ access ค่าของ query ต้อง access ผ่าน this.props.location.query เราก็จะได้ object ที่มี playerOne และ playerTwo เป็น properties อยู่ภายใน

ดังนั้น ภายในฟังก์ชั่น componentDidMount เราจะใช้ this.props.location.query เพื่อดึงข้อมูลจาก query มาใช้งานในครั้งต่อไป

สำหรับผู้ที่อยากได้ code ต้นแบบสำหรับอ้างอิง ให้ไปดูได้ที่

https://github.com/himaeng/react-fun-course/tree/master/06-life-cycle

สรุป

เนื้อหาตอนนี้เราว่ากันด้วยเรื่องของการจัดการ Life Cycle ของ React อย่างละเอียด

เราได้รู้แล้วว่า Life Cycle ของ React แบ่งออกเป็นสองกลุ่ม กล่าวคือ

  1. กลุ่มของการ mount/unmount ที่เกี่ยวข้องกับ DOM
  2. กลุ่มของการจัดการ data

โดยกลุ่มแรกนั้นเราจะใช้ในการเรียกใช้ฟังก์ชั่นที่ก่อให้เกิด side effect ทั้งหลายเช่นการ call remote service หรือการ set state/props ทั้งหลาย

ส่วนกลุ่มที่สองจะเกี่ยวกับการจัดการเรื่องการ render ui