React Native ListView

รอบนี้เราจะเอา ListView ของ React Native ซึ่งเป็น component ที่ใช้สำหรับการแสดง list มาใช้งาน

ให้เปิดไฟล์ tech_stack/src/components/LibraryList.js ขึ้นมา แล้วปรับแก้ไฟล์ดังนี้

พอเอาไปเปิด emulator ขึ้นมาจะได้หน้าตาดังรูป

screen-shot-2559-12-06-at-6-53-57-am

หน้าตาของ code ภายใน componentWilMount อาจจะดูแปลกๆ หน่อย แต่ไม่ต้องสนใจ เพราะมันเป็น code ที่ใช้สำหรับ setup ตัว ListView อยู่แล้ว ไม่เชื่อไปดูจากเพจของ Facebook ได้เลย เราก็มีหน้าที่แค่ทำตามมันไป เปลี่ยนแค่ส่วนของ logic ในการ renderRow ก็เท่านั้น

คราวนี้ผมอยากจะแยกตัว jsx ที่ใช้สำหรับ render row ให้กับ ListView ออกมาเป็น component แยกต่างหาก ด้วยเหตุนี้ผมจึงต้องสร้างไฟล์ใหม่ขึ้นมาคือ tech_stack/src/components/ListItem.js ทำให้โครงสร้างไฟล์จะเป็นดังนี้

จากนั้นก็จัดเต็มให้ component ตัวนี้กันเลย

เนื่องจาก component ตัวนี้มีหน้าที่ในการแสดงผลอย่างเดียว ดังนั้นผมจึงเลือกใช้วิธี stateless functional ในการสร้าง component ตัวนี้ขึ้นมา โดยมีการเพิ่ม style และครอบด้วย <CardSection/> เข้าไปอีกต่อหนึ่งเพื่อความสวยงามมากขึ้น

จากนั้นให้นำ component ตัวใหม่เข้ามาใช้ใน LibraryList component โดยให้เปิดไฟล์ tech_stack/src/components/LibraryList.js ขึ้นมาแล้วแก้ code ดังนี้

เมื่อเปิด emulator เราจะได้ app หน้าตาประมาณนี้

screen-shot-2559-12-06-at-7-09-06-am

ให้เราลองใช้เมาส์ไป drag ที่ list จากนั้นลากลงมา จะเห็นว่า list มันจะหายไปตรงกลางจอ ดังรูป

จากรูป ตอนผมลาก list ลงมา (เปรียบไปก็เหมือนกับการ scroll ตอนใช้ app ใน mobile จริง) item สองตัวสุดท้าย กล่าวคือ Babel กับ Axios จะหายไป ตรงกลางจอ นั่นแปลว่า ตัว ListView ไม่ได้ถูกขยายเต็มหน้าจอ

ดังนั้น เราจะแก้ปัญหานี้ตรง component ระดับที่ห่อหุ้มตัว LibraryList ซึ่งอยู่ที่ tech_stack/src/App.js ดังนี้

เราใส่ flex: 1 เข้าไปให้กับ View ทำให้ View มันขยายเต็มหน้าจอส่งผลให้ ListView ขยายไปจนเต็มหน้าจอด้วยเช่นกัน คราวนี้เวลาเราลาก list ลงมาก็จะไม่หายไปที่กลางจอเหมือนเดิมอีกแล้ว

Selection Reducer

กลับมาดูข้อมูลของ reducer และ state ที่เราจำเป็นต้องใช้เพิ่มเติมกันเสียหน่อย

reducer

จากรูปจะเห็นว่า เรายังไม่ได้สร้าง Reducer ที่ชื่อ Selection เลย ซึ่ง reducer ตัวนี้จะมีหน้าที่ในการบันทึกว่า ตอนนี้ มีรายการไหนใน List ที่ถูกเลือกบ้าง จากนั้นมันจะจำ id ของ item ตัวนั้นไว้

ให้เราสร้าง reducer ตัวใหม่ขึ้นมาที่ tech_stack/src/reducers/SelectionReducer.js ทำให้ตอนนี้โครงสร้าง directory ของเราจะเป็นดังนี้

จากนั้นให้กรอก code เข้าไปใน SelectionReducer.js ดังนี้

ตอนนี้เรายังไม่ต้อง implement รายละเอียดของ SelectionReducer แค่สร้างโครงไว้ก่อน

จากนั้นให้นำ SelectionReducer ไปรวมไว้ใน combineReducers โดยให้เปิดไฟล์ tech_stack/src/reducers/index.js ขึ้นมาแล้วแก้ code ดังนี้

เพียงเท่านี้ ระบบของเราก็จะมี reducer เข้ามาสองตัวแล้ว (เปรียบไปแล้วก็เหมือนกับธนาคารจ้างพนักงานแบงค์เพิ่มอีกหนึ่งตำแหน่ง โดยต้องเข้ามารับหน้าที่ที่ต่างออกไปจากตำแหน่งเก่า)

Action Creator

ยังจำกันได้มั๊ยครับ ถึงเสาหลักอีกตัวหนึ่งของ Redux ซึ่งก็คือ Action Creator หรือท่อนำฝากที่ใส่สลิปฝาก/ถอน และเงินสด ไว้ภายในนั่นเอง

ถึงเวลาที่เราจะเอา concept ตัวนี้มาใช้งานจริงกันแล้ว

ให้เราสร้าง folder ใหม่ภายใต้ tech_stack/src ชื่อว่า actions แล้วภายใต้โฟลเดอร์นั้นให้สร้างไฟล์ชื่อ index.js ขึ้น ทำให้ตอนนี้โครงสร้าง directory ของระบบจะเป็นดังนี้

จากนั้นให้เพิ่ม code  เข้าไปใน index.js ดังนี้

เพื่อป้องกันความงงงวย ให้นึกไว้เสมอว่า Action Creator คือท่อนำฝากที่ภายในบรรจุ สลิปฝาก/ถอน และเงินสด หรือ action เอาไว้ ซึ่งในที่นี้ เราได้สร้าง Action Creator ที่ชื่อว่า selectLibrary ที่ทำหน้าที่เป็นท่อนำฝากที่ใช้บรรจุ action ที่มี type เป็น select_library และมีข้อมูลแนบเป็น libraryId นั่นเอง

สั่งเกตว่า Action Creator (ท่อนำฝาก) มันเป็นแค่ฟังก์ชั่นโง่ๆ ที่ใช้สำหรับ return action ตรงๆ เพียงแค่นั้น ไม่ได้มีอะไรซับซ้อน เปรียบไปแล้วก็ไม่ต่างอะไรกับท่อนำฝากที่ผมได้เคยกล่าวไว้

คราวนี้ก็มาถึงหัวข้อว่า แล้วใครจะเป็นคนที่เรียกใช้บริการฟังก์ชั่นของ Action Creator (พูดเป็นภาษาเปรียบเปรยก็คือ ใครคือลูกค้าที่จะมาเข้ามาใช้บริการตัวนี้) คำตอบก็คือ ListItem component นั่นเอง เพราะมันคือคนที่จะส่งข้อมูล id ของตัวมันไปให้ Reducer (พนักงานแบงค์) ว่า ตัวมันถูก select อยู่หรือไม่

ให้เปิดไฟล์ tech_stack/src/components/ListItem.js แล้วแก้ไข code ดังนี้

สิ่งที่อยากให้สังเกตก็คือ component ตัวไหนก็ตามที่มีความต้องการจะใช้บริการ (action) จาก Redux จะต้อง import connect จาก react-redux มาใช้เสมอ เพราะอย่างที่ผมได้เคยพูดไว้ หากอยากจะ access ข้อมูล state ภายใน store ของ Redux มีทางเดียวเท่านั้นคือ ต้องทำผ่าน connect

เราจึงต้อง connect ListItem ไว้กับ Redux แต่คราวนี้ syntax ในการเรียกใช้งาน connect จะแปลกไปซักหน่อยนึงตรงที่ จะมีการกรอก argument สองตัว ซึ่งตัวแรกเราได้เรียนรู้ไปแล้วว่า ทำหน้าในการ map state ของ Redux ไปยัง component ปลายทาง แต่เนื่องจากใน ListItem ยังไม่มีความจำเป็นต้องใช้งาน state ใดๆ เราจึงใส่เป็นค่า null ไป

ส่วนที่สำคัญก็คือ argument ตัวที่สอง ซึ่งทำหน้าที่ในการนำ action creator ทั้งหลายมา map ให้เป็น props อีกชุดหนึ่งเพื่อ “ฉีด” เข้ามาใน component ปลายทาง

ฉะนั้น connect จะทำหน้าที่ในการ map ทั้ง state และ action creator เพื่อฉีดเข้าไปเป็น props ภายใน component ปลายทางทั้งหมด

คราวนี้ให้เราลองเปิด emulator แล้ว debug ดู จะเห็นว่า ตอนนี้ props ของ ListItem (ที่เราเขียนดักด้วย console.log เอาไว้) จะมีอยู่สองค่า กล่าวคือ library และ selectLibrary ดังรูป

screen-shot-2559-12-06-at-8-25-28-am

Adding TouchableWithoutFeedback component

เราจะใช้ component ของ React Native มาช่วยเพื่อทำให้ ListItem สามารถคลิกได้เพื่อ expand รายละเอียดของแต่ละ item

ดังนั้นให้เปิดไฟล์ tech_stack/src/components/ListItem.js ขึ้นมา แล้วเพิ่ม TouchableWithoutFeedback เข้าไปใน component ด้วยการแก้ code ดังนี้

เราจะนำ action creator ที่ชื่อ selectLibrary มาเป็น callback ให้กับ onPress event ของ TouchableWithoutFeedback แล้วยิง id ของ library ไปเป็น parameter ภายใน callback

จากนั้นให้เราเปิดไฟล์ tech_stack/src/reducers/SelectionReducer.js แล้วแก้ code ดังนี้

โดยปรกติแล้ว action creator จะรับ argument 2 ตัว กล่าวคือ state จาก Redux และ action ได้ เราก็ลองดูซะหน่อยว่า action ที่ถูกส่งเข้ามาใน action creator ตัวนี้คืออะไร ด้วยการดัก console.log ไว้

พอเปิด emulator แล้ว debug จากนั้นคลิกที่รายการแต่ละรายการบน list แล้วหน้า console จะแสดงผลดังภาพต่อไปนี้

screen-shot-2559-12-06-at-9-06-22-am

ข้อมูลใน action จะมีอยู่สองตัวคือ type (สลิป/ฝากถอน) และ payload (เงินสด) สำหรับคราวนี้เวลาที่เราคิดที่ item มันจะส่ง id ของมันแนบไปกับ action ด้วย

เพิ่ม rule ให้กับ SelectionReducer

คราวนี้ได้เวลาเขียน rule ให้กับ SelectionReducer แล้ว เปรียบไปแล้วก็เหมือนกับการเขียน job description ใหักับ พนักงานแบงค์ฝ่ายนี้นั่นเอง

ให้เราเปิดไฟล์ tech_stack/src/reducers/SelectionReducer.js ขึ้นมาแล้วปรับ code ดังนี้

สังเกตมั๊ยครับว่า parameter state นั้น เราต้องกำหนดค่าตั้งต้นให้เป็น null เสียก่อน เพราะ action creator ต้อง return ค่า state ของมันเสมอ แม้จะเป็นค่า null ก็ได้ แต่ห้าม return เป็น undefined เด็ดขาด ดังนั้นเราจึงต้องเขียน state ให้มี default value เอาไว้

วิธีเขียน rule ของ Reducer นั้นมักจะใช้ switch เพื่อเช็คดูว่า action.type ที่เข้ามาตรงกับเงื่อนไขที่ตั้งไว้หรือไม่ หากตรงก็ดำเนินการทันที เปรียบเหมือนกับพนักงานแบงค์ที่เปิดท่อนำฝากออกดูใบสลิป หากมันไม่ตรงกับหน้าที่ของตน ก็ส่งผ่านไปยังพนักงานคนที่เกี่ยวข้องต่อไป แต่ตัวเองไม่ต้องทำ

ให้เราลองเปิด emulator ขึ้นมาเพื่อดูว่า มันยังทำงานได้ตามปรกติอยู่หรือไม่

ทำให้ ListItem สามารถ expand ได้

ตอนนี้ state ของระบบมีอยู่สองตัวนั่นคือ libraries ซึ่งมาจาก LibraryReducer และ selectedLibraryId ซึ่งมาจาก SelectionReducer ที่เราเพิ่งสร้างไป โดยทั้งหมดนี้จะไป map กันที่ไฟล์ tech_stack/src/reducers/index.js (ลองไปเปิดดูอีกครั้งแล้วจะเข้าใจกระบวนการ)

ตอนนี้เราต้องการให้ ListItem นั้นจะทำการ expand ออกมาเพื่อแสดง description หากถูกคลิก ดังนั้น ก่อนอื่นเราจะต้องทำการเปรียบเทียบก่อนว่า ค่า state.selectedLibraryId ซึ่งเป็นค่า id ที่ถูกเลือกนั้น มันตรงกับค่า props.id ของ ListItem ตัวไหนบ้าง หากตรงกับตัวไหน ให้มันแสดง description ออกมา

ให้เปิดไฟล์ tech_stack/src/components/ListItem.js ออกมาแล้วแก้ไข code ดังนี้

เนื่องจากผมเพิ่งรู้ตัวว่า ListItem นั้น ต้องมี logic ภายในตัวมันอยู่เยอะพอสมควร ดังนั้น ผมจึงต้องเปลี่ยนวิธีการ implement จาก stateless functional มาเป็น class ดังนั้น เลยมีการ refactor เยอะหน่อย

ภายใน component เราได้เพิ่มฟังก์ชั่น renderDescription ขึ้นมา เพื่อใช้สำหรับรองรับการ render ให้ ListItem แสดงผล description บนหน้าจอ หากค่า id ของ Item ตัวนี้ตรงกับ state.selectedLibraryId หรือ id ที่ถูกเลือก มันจะทำการแสดง description ออกมา

และเนื่องจากงานนี้เราต้องการ access ค่า selectedLibraryId จาก state ของ Redux ดังนั้น เราจึงเขียนฟังก์ชั่น mapStateToProps ขึ้นมาเพื่อดึงค่าดังกล่าว แล้วให้ connect ทำการฉีด props ดังกล่าวเข้าไปใน ListItem

เมื่อลอง refresh emulator แล้ว ให้ลองคลิก item บางตัวดู จะเห็นว่า มันสามารถแสดง description ขึ้นมาได้ ดังรูป

screen-shot-2559-12-06-at-10-07-49-am

ListItem Refactoring

คราวนี้เราต้องการจะจับเอา logic ในการตรวจสอบว่า listitem นี้ควรจะ expand หรือไม่ ออกมาจากตัว component

วิธีการก็คือ เอา logic ดังกล่าว มาไว้ใน mapStateToProps แทน วิธีการจะเป็นดัง code ต่อไปนี้

สำหรับ mapStateToProps นั้น แท้ที่จริงแล้ว สามารถรับ argument ได้มากกว่า 1 ตัว โดยก่อนหน้านี้เราจะมีแค่ state argument ตัวเดียว แต่คราวนี้เราจะใส่ argument ตัวที่ 2 เข้าไปด้วย

เจ้า argument ตัวที่สองนี้คือ props ของตัว component ปลายทางนั่นเอง แปลว่า mapStateToProps นั้นสามารถ access ได้ทั้ง state ของ Redux และ props ของ component ปลายทางได้ในเวลาเดียวกัน

ดังนั้น เรานึงสามารถย้ายขั้นตอนการตรวจสอบว่า id ตรงกับ selectedLibraryId หรือไม่ มาอยู่ที่ mapStateToProps ได้ดัง code ข้างต้น

คราวนี้ให้เราลอง refresh emulator อีกทีเพื่อดูว่า มันยังทำงานได้ตามเดิมไม่มี error ใดๆ

Style and Animation

เราจะมาทำให้ ListItem ดูดีขึ้น โดยการใส่ style และ animation เข้าไปช่วย ซึ่งวิธีการก็ง่ายมากครับ ให้เปิดไฟล์ tech_stack/src/components/ListItem.js แล้วแก้ไขดังต่อไปนี้

เรื่อง style คงไม่ต้องพูดแล้วเพราะมันก็ตรงไปตรงมา ส่วน animation นั้น เราจะดึงมาจาก LayoutAnimation ของ react-native จากนั้นก็แค่เขียน code LayoutAnimation.spring(); เข้าไปใน componentWillUpdate() เพื่อให้ animation ตัวนี้แสดงผลในช่วงก่อนที่ component จะได้รับการ update เพียงเท่านี้ก็เรียบร้อย

เมื่อเปิด emulator ขึ้นมาก็จะเห็นหน้าจอดังนี้

screen-shot-2559-12-06-at-10-34-55-am

สำหรับผู้ที่ต้องการจะเข้าไปดู code ต้นแบบ สามารถเข้าไปดูได้ที่

https://github.com/himaeng/tech_stack.git

สรุป

จบไปอีกหนึ่งโปรเจ็กทแล้วนะครับ สำหรับ tech_stack อันเป็นโปรเจ็กท์ที่ใช้เพื่อสอนเรื่อง Redux ขั้นต้นให้พวกเรามีความคุ้นเคยกับมันในระดับนึง

เรามาทบทวนขั้นตอนการทำงานกับ redux กันหน่อย

  1. สร้าง store ภายใน Provider
  2. จากนั้นสร้าง reducers เพื่อนำไปใช้ใน store
  3. ภายใน reducer จะใช้ combineReducers เพื่อใช้ในการรวบ reducer จำนวนหลายๆ ตัวให้มาอยู่ในที่เดียวจากนั้นก็นำไปใช้ใน store ที่เราได้สร้างไว้ในขั้นที่ 1
  4. ภายใน reducer มักจะสร้างโดยใช้ switch เพื่อเลือกว่า เราจะทำงานใด กับ action.type ใด
  5. สร้าง action creator ซึ่งเป็นฟังก์ชั่นที่ return action ออกมา
  6. ภายในแต่ละ component ที่ต้องการ access state ของ Redux ให้เราสร้าง mapStateToProps แล้วนำไปฉีดใส่ component ผ่าน connect ฟังก์ชั่น
  7. หาก component ตัวใดที่ต้องการใช้งาน action creator ก็ให้ใส่ action creator เข้าไปใน argument ตัวที่สองใน connect เพื่อ map action creator ให้กลายเป็น props แล้วฉีดเข้าไปใน component ปลายทางเช่นกัน

Redux อาจจะไม่ง่ายที่จะเข้าใจนัก แต่หลังจากนี้เราจะนำ Redux มาใช้ซ้ำแล้วซ้ำอีก จนคุณผู้อ่านเริ่มมีความคุ้นเคย และคล่องแคล่วกับเฟรมเวิร์คตัวนี้