List App with Redux

เรามาเริ่มลงภาคปฏิบัติ Redux กันเลยดีกว่า

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

reduxmock

เราจะทำ app ที่มี list ของ technology stack ของระบบ React โดยมีคำบรรยายแทรกเพื่ออธิบายว่าแต่ละเทคโนโลยีภายใน stack ของ React นั้น มีคุณสมบัติ และทำหน้าที่อะไรบ้าง

App. ตัวนี้จะใช้ Redux เป็นเฟรมเวิร์คหลักในการพัฒนา แต่ก่อนอื่น

ให้เรา init react native ดังนี้

สร้างไฟล์ .eslintrc

ให้สร้างไฟล์ที่ root path ของ project albums ชื่อว่า .eslintrc แล้วเขียน code ดังนี้

ลองเล่น Redux ดูซักหน่อย

ก่อนจะลองนำความรู้ Redux มาใช้งานจริงนั้น เรามาลองเขียน code redux เล่นๆ กันดูซักหน่อย แบบไม่ต้องสร้าง project ขึ้น เมื่อซ้อมมือเล่น

เราจะไปเล่นกันที่ https://stephengrider.github.io/JSPlaygrounds/ ซึ่งเป็น app ให้เราลองเขียน code Redux เล่นได้เลย โดยไม่ต้องติดตั้งอะไรให้ยุ่งยาก

ในส่วนของ pane ทางซ้ายให้กรอก code ดังต่อไปนี้

อันนี้เป็นการ setup ให้ redux สามารถทำงานบน React ได้ โดยเราจะเริ่มต้นด้วย code เพียงแค่นี้

ซึ่งตรงนี้จะมีศัพท์ใหม่มาอีกหนึ่งตัว (อีกแล้ว 555) นั่นคือ Store

ยังจำตัวอย่างการฝากเงินได้มั๊ยครับ ที่ต้องมีสลิป (action) มีท่อนำฝาก (action creator) และมีพนักงานแบงค์ (reducer) มาทำงานร่วมกันจึงจะครบองค์ประชุม แต่อย่าลืมนะครับว่า พนักงานแบงค์ (reducer) จะต้องทำงานอยู่ในห้องทำงานภายในธนาคาร และเจ้าห้องทำงานนี่แหละที่เราเรียกกันว่า store หรือคลังเก็บ reducer และ state ต่างๆ (store ในที่นี้ไม่ได้แปลว่าร้านค้านะครับ แต่แปลว่าคลังเก็บ) หน้าที่ของ store นั้นคือเป็นตัวกลางระหว่าง action และ reducer ให้มันทำงานร่วมกันได้แค่นั้น

ส่วนเจ้าคำสั่ง store.getState() นั้นคือ การร้องดูค่า state ล่าสุดจาก store นั่นแปลว่า Redux ไม่ให้เราติดต่อกับ reducer โดยตรง แต่ต้องติดต่อรับข้อมูลผ่าน “หน้าห้อง” ซึ่งก็ไม่แปลก เพราะหากว่ากันตามระบบแบงค์แล้ว เขาก็ไม่ให้ลูกค้า หรือพนักงานภายนอกติดต่อพนักงานแบงค์ตรงๆ เป็นแน่ แต่จะต้องให้ติดต่อผ่าน “หน้าห้อง” แทน

ดังนั้น ตอน setup Redux เราจะต้องสร้าง store ขึ้นมาก่อน โดยจะต้องแจ้ง store ด้วยว่า ภายในห้องทำงานแห่งนี้ จะมี reducer (พนักงานแบงค์) ตัวไหนอยู่ภายในห้องนั้นบ้าง ด้วยคำสั่ง

จาก code ข้างต้นจะเห็นว่า ผลลัพธ์ของการเรียกใช้ store.getState() คือ [] หรือ empty array เพราะ ตอนนี้ reducer ยังไม่ได้ทำหน้าที่ใดๆ นอกจากการส่งคืนค่า empty array ออกมาเท่านั้น

ดังนั้น เรามาตั้ง job description ให้กับพนักงานแบงค์คนนี้ดีกว่า (reducer) โดยเพิ่ม code ไปดังนี้

ตอนนี้หน้าที่ของพนักงานแบงค์ (reducer) คือการกระจาย string ให้กลายเป็น array โดยนำ data ภายใน action.payload มาใช้ ในกรณีที่ action.type (คำสั่งว่าเป็นใบนำฝาก หรือใบถอนเงิน) คือ ‘split_string’ แต่หากเป็น type อื่นก็ให้ return state เดิมกลับไป

สังเกตว่า code ข้างต้นก็ยังคง return ค่าสุดท้ายออกมาเป็น empty array เหมือนเดิม เพราะเรายังไม่ได้ยิง action (สลิปฝาก/ถอน) ให้กับ reducer (พนักงานแบงค์) เลย แต่เราเซตค่าตั้งต้นให้ state มีค่าเป็น empty array ([]) แต่ผมจะไม่บอกว่าผมเซตไว้ตรงไหน ให้ไปหาเอาเองเป็นแบบฝึกหัดว่าทำไม state ถึงมีค่าตั้งต้นเป็น []

คราวนี้เราจะเริ่มสร้าง action (สลิปฝาก/ถอน) ตัวแรกกัน

ตอนนี้เราได้สร้าง action ที่มี type เป็น ‘split_string’ พร้อมกับ “เงินสด” หรือ payload ซึ่งเป็น data ที่เราจะพ่วงเข้าไปพร้อมกับ action ด้วย

คราวนี้ให้ลองเพิ่ม code เข้าไปสองบรรทัด ดังนี้

จะเห็นว่า ด้านขวาของจอ จะแสดงผลดังนี้

บรรทักแรกแสดงให้เห็นว่า เวลาเราสั่ง store.dispatch นั้น มันจะยิง action เข้าไป ซึ่งการ dispatch ก็คือการ “ดูด” ท่อนำฝากเข้าไปในระบบท่อสุญญากาศนั่นเอง

จากนั้นในบรรทัดที่สองก็เกิดจากเรียก store.getState() หรือการแสดง state ปัจจุบันหลังจากที่ reducer ได้ทำการเปลี่ยนแปลงข้อมูลใน state ไปแล้ว

คราวนี้เราจะมาเพิ่ม job description ให้กับพนักงานแบงค์ของเราคนเดิมอีกนึ่งหน้าที่

คราวนี้เราเพิ่มงาน ‘add_character’ เพิ่มเข้าไปอีกหนึ่งหน้าที่ โดยเนื้องานก็ไม่มีอะไรมาก แค่ add ข้อมูลภายใน action.payload เข้าไปใน state เท่านั้นเอง

เมื่อเรากำหนด job description แล้ว ก็ต้องลองสั่งงานน้องพนักงานแบงค์ซักหน่อย

ผลที่ได้ก็จะเป็นไปตามคาดครับ

ติดตั้ง Redux

เปิด terminal มาแล้วเข้าไปที่โฟลเดอร์ tect_stack ที่เราเพิ่งสร้างไปในตอนต้น จากนั้นพิมพ์คำสั่งต่อไปนี้

ในที่นี้ เราจะ install 2 modules สาเหตุที่ต้องมีทั้ง redux และ react-redux นั้นเป็นเพราะว่า redux เป็น pattern ในการบริหาร app ซึ่งสามารถนำไปใช้กับเฟรมเวิร์คไหนก็ได้ทั้งนั้น ดังนั้น หากเราจะนำมาใช้กับ React ก็ต้อง install react-redux ด้วย เพื่อให้ react รู้จักกับ redux

จากนั้นก็วางโครงให้กับโปรเจ็กท์ซักหน่อย

ให้สร้างโฟลเดอร์ src ขึ้นมาที่ root project แล้วภายใน src ให้สร้างไฟล์ App.js ซึ่งจะทำให้มีโครงสร้างไฟล์ ดังนี้

 

โดยเขียน code ดังนี้

จากนั้นให้แก้ไฟล์ index.android.js และ index.ios.js ดังนี้

Create Redux’s Store

คราวนี้ได้เวลา setup ให้ app ของเราสามารถใช้งาน Redux ได้แล้ว โดยเราจะเริ่มต้นจากการตั้ง “ห้องพนักงานแบงค์” ขึ้นมาก่อน (เหมือนก่อนเปิดบริษัทก็ต้องสร้างตึกก่อนแล้วค่อยจ้างคน) ให้เปิดไฟล์ tech_stack/src/App.js แล้วเพิ่ม code สำหรับ store ดังนี้

งานนี้ผู้อ่านอาจจะเริ่มเซ็งจิต ตรงที่ “แม่งมีตัวใหม่มาอีกแล้วเหรอวะ” เพราะงานนี้เรามี Provider เพิ่มเข้ามาด้วย

คำถามคือ “แล้วไอ Provider มันคือ here อะไรครับ?” ใจเย็ดๆ ครับ เดี๋ยวผมอธิบายให้ฟัง รับรองเจ็บนิดเดียว

อย่างที่ผมบอกไปแล้วในตอนติดตั้ง Redux ว่าเฟรมเวิร์คตัวนี้เป็นเฟรมเวิร์คกลางที่สามารถนำไปใช้งานกับ JS framework ตัวไหนก็ได้ ซึ่งหลักการก็คล้ายๆ กับ react, react-dom, react-native นั่นแหละ กล่าวคือ react เป็น library กลาง ที่สามารถนำไปใช้กับ แพลดฟอร์มอะไรก็ได้ (web, mobile) แต่เวลานำไปใช้จริง ต้องมี module เฉพาะมาช่วย เช่น หากต้องการพัฒนา React บนเว็บก็ต้องใช้ react-dom และหากเราต้องการพัฒนา React Mobile App. เราก็ต้องใช้ react-native

Redux สำหรับ React ก็จำเป็นต้องใช้ redux-react มาช่วย ซึ่งเจ้า redux-react นี้จะมี Provider component มาให้ ซึ่งหน้าที่ของมันจะเป็นไปตามแผนภาพดังต่อไปนี้

screen-shot-2559-12-03-at-8-16-18-am

นั่นคือ มันจะเป็นตัวกลางที่ทำให้ React สามารถทำงานกับ Store ของ Redux ได้เท่านั้นเอง สรุปง่ายๆ ก็คือ เป็นตัวกลางที่ทำให้ React สามารถทำงานร่วมกัน Store ของ Redux ได้เท่านั้นเอง โดยหากดูตามรูป จะเห็นการเปลี่ยนแปลงในโครงสร้างของโปรเจ็กท์ React เล็กน้อย จากแต่เดิม App component จะทำหน้าที่เป็น root component มาตอนนี้ Provider และ Store จะกลายมาเป็น Root component แทน

ดังนั้นเวลา setup Redux ภายใน App component เราจึงต้องเอา Provider มาครอบ component ภายในอีกที ดังนี้

Create Reducer

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

ภายในไฟล์ index.js ให้เติม code ดังนี้

แบงค์ต้องมีพนักงานหลายคน เช่นเดียวกัน Store ก็ต้องมีหลาย Reducer ดังนั้น เวลาจะ “มัด” รวม reducer หลายๆ ตัวให้รวมเป็นก้อนเดียวเพื่อยัดเข้าไปใน Store ให้ได้นั้น จะต้องใช้ combineReducers จาก redux มาทำหน้าที่นี้ โดยนำ reducer ทั้งหมดที่เรามีใส่เข้าไปใน array ภายใน combineReducers ซึ่งตอนนี้เรายังไม่ได้ใส่ reducer ใดๆ เข้าไป เพียงใส่เป็น empty array ไปก่อน

จากนั้นก็ให้เอาตัว combineReducers นี้ import เข้าไปใช้ใน App component เพื่อใช้งานกับ Store (คือคล้ายกับเราต้องระบุว่า ออฟฟิศห้องนี้ มีใครทำงานภายในนั้นบ้าง) ให้เปิดไฟล์ tech_stack/src/App.js ขึ้นมาแล้วเพิ่ม code ดังนี้

กุญแจสำคัญอยู่ที่ code บรรทัดนี้

แปลเป็นภาษาคนได้ว่า

สร้าง Store ที่เก็บ reducer ต่างๆ ไว้ภายใน โดยใช้ provider เป็นตัวกลางเพื่อเชื่อมการทำงานกับ React

Reusing common components

สร้าง folder tech_stack/src/components/common (สร้างสอง folder ซ้อนกัน) แล้วจากนั้นให้ทำการ copy ไฟล์ทั้งหมดใน auth/src/components/common/ มาวางไว้ในโฟลเดอร์ที่เพิ่งสร้างใหม่นี้ ทำให้ตอนนี้โครงสร้างไดเรคทอรี่ของโปรเจ็กท์ tech_stack เป็นดังนี้

จากนั้นก็เอา Header component มาใช้ใน App component โดยให้เปิดไฟล์ tech_stack/src/App.js แล้วเพิ่ม code เข้าไปดังนี้

หมายเหตุ: ภายใต้ Provider component อนุญาติให้วางได้เพียง component เดียวเท่านั้น ดังนั้น เราจะไม่สามารถเขียน code แบบนี้ได้

นี่จึงเป็นสาเหตุให้เราต้องครอบด้วย <View> ไว้ แล้วภายในจะวาง component อะไรก็ค่อยวางไป

จากนั้นให้เปิดรัน command ต่อไปนี้เพื่อเปิด emulator

screen-shot-2559-12-03-at-3-54-19-pm

Reducer and State design

ในระบบ tech_stack นี้จะมี reducer ใหญ่ๆ อยู่สองตัว นั่นคือ  Library และ Selection reducer โดยมีรายละเอียดดังรูป

reducer

Library Reducer จะทำหน้าที่ในการแสดงรายการทั้งหมดที่จะแสดงบนหน้าจอ ส่วน Selection Reducer จะทำหน้าที่ในการขยาย library ที่ถูกเลือก โดยเก็บค่าเป็น id ของ Library

ให้เราสร้างไฟล์ tech_stack/src/reducers/LibraryReducer.js ขึ้นมา แล้วกรอก code ดังนี้

จากนั้นเปิดไฟล์ tech_stack/src/reducers/index.js ขึ้นมาแล้วแก้ code ดังนี้

ซึ่งก็คือการนำ LibraryReducer.js เข้ามาทำหน้าที่แสดง list ให้กับ state ที่ชื่อ libraries นั่นเอง

ตอนนี้หากเราทำ console.log เพื่อดูค่า state ภายใน store (ออฟฟิศของพนักงานแบงค์) เราจะได้ค่าประมาณนี้

สรุปคือ ตอนนี้ state ของ store ในปัจจุบันของเราจะมี state หนึ่งตัวที่ชื่อว่า Libraries

คราวนี้เราจะทำการ refactor code ของ LibraryReducer.js ซักเล็กน้อย โดยจะแยกส่วนของข้อมูลดิบออกมาเป็นไฟล์แยกต่างหาก

ให้เราสร้างไฟล์ tech_stack/src/reducers/LibraryList.json แล้วเพิ่ม code ดังนี้

จากนั้นให้กลับไปแก้ไฟล์ tech_stack/src/reducers/LibraryReducer.js ดังนี้

ตอนนี้โครงสร้าง directory จะเป็นดังนี้

Connect Function

คราวนี้ได้เวลาสร้าง component สำหรับแสดง list ของ library กันแล้ว

ให้สร้างไฟล์ tech_stack/src/components/LibraryList.js ขึ้นมา ทำให้ตอนนี้โครงสร้างไฟล์เราจะเป็นดังนี้

ให้ใส่ code ตั้งต้นไปดังนี้

สถาปัตยกรรมของ LibraryList component จะเป็นดังต่อไปนี้

screen-shot-2559-12-05-at-11-33-26-am

LibraryList เป็น component ลูกของ App โดย root component คือ Provider ซึ่งภายในจะมี Store (ออฟฟิศพนักงานแบงค์) ที่เก็บ state อยู่สองตัว { libraries, selectedLibraryId }

Redux บังคับให้เรา access state data ได้ที่เดียวเท่านั้น คือ “เดินไปขอข้อมูลหน้าห้องออฟฟิศ” หรือดึงข้อมูลจาก store โดยผ่านฟังก์ชั่นตัวกลางที่ชื่อว่า connect (จริงๆ แล้ว connect ก็เหมือนพวก messenger ที่มีหน้าที่คอยส่งเอกสารภายในบริษัทนั่นเอง) ซึ่งมีแนวการทำงานดังนี้screen-shot-2559-12-05-at-11-34-34-amเมื่อใดก็ตามที่ LibraryList ต้องการข้อมูลจาก state ก็จะต้องเรียกใช้งานเมสเซนเจอร์ (Connect) ให้วิ่งไปขอข้อมูลจากหน้าห้องออฟฟิศ (Store) นี่คือ กฎของธนาคารแห่งนี้ (Redux) ใครก็มิอาจฝ่าฝืนได้

ดังนั้น ให้เปิดไฟล์ tech_stack/src/components/LibraryList.js ขึ้นมาแล้วใส่ connect เข้าไปทำหน้าที่ดังนี้

อาจจะรู้สึกแปลกๆ หน่อยกับ code บรรทัดนี้

คำแนะนำของผมก็คือ “จำๆ ไปซะ” ไม่ต้องไปทำความเข้าใจอะไรหรอกครับ เพราะมันก็แค่ syntax สำหรับใช้งาน connect เท่านั้น

Mapping Redux State to Props

หลังจากที่เราให้ connect วิ่งไปเอาข้อมูล state จาก Redux store มาแล้วมันจะมีวิธีในการแปรข้อมูลดังกล่าวเอามาใช้ใน component อยู่วิธีหนึ่ง เรียกว่า mapStateToProps

มาดู code ของจริงกันเลยดีกว่า

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

อธิบาย step การทำงานของ connect และ mapStateToProps ดังนี้

  1. connect วิ่งไปเอา state จาก Store
  2. state ที่ได้มาจะถูกนำมาใส่ลงไปเป็น argument ให้กับ mapStateToProps
  3. mapStateToProps ทำการ map ค่า state ที่ได้มาบางค่าที่จำเป็นกับ component นั้นๆ แล้ว return ออกมา
  4. connect จะทำการ “ฉีด” props ที่ mapStateToProps ได้ return ออกมาเข้าไปใน component ปลายทาง

แผนภาพดังต่อไปนี้ จะแสดงภาพรวมของขั้นตอนทั้งสี่นี้

mapstatetopropsในที่นี้เราได้ทำการดัก console.log ไว้ภายใน mapStatetoProps ซึ่งเมื่อรัน emulator แล้วเปิด debug แล้วจะเห็นข้อมูลดังนี้

screen-shot-2559-12-05-at-12-52-59-pm

จะเห็นว่าตอนนี้ state ของเราจะมีข้อมูลเพียงชุดเดียวนั่นคือ libraries ซึ่งเป็น array ที่มีสมาชิก 9 ตัว

คราวนี้เราลองทำการ map state จาก Redux ไปยัง component ดูกันบ้าง

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

โดยเราจะทำการ map ค่าของ state.libraries ที่ดึงมาจาก Redux ให้ไปเป็น props ที่ชื่อ libraries ของ LibraryList component

คราวนี้ลองเปิด emulator แล้ว debug ดู คุณผู้อ่านจะเห็นภาพดังรูปต่อไปนี้

screen-shot-2559-12-05-at-9-21-46-pm

จะเห็นว่า props ของ LibraryList นั้นจะมีสองตัว คือ dispatch (ซึ่งผมจะพูดถึงในภายหลัง) และ libraries

บทนี้ขอจบดื้อๆ แบบนี้ไปก่อนนะครับ เพราะมันยาวมาแล้ว เดี๋ยวเรามาจัดเต็มกันตอนหน้าต่อเลย

สรุป

บทนี้เป็นเนื้อต่อจาก Redux 101 ในตอนที่แล้ว โดยลงภาคปฏิบัตการกันเลย ซึ่งเนื้อหาในตอนนี้จะครอบคลุมเนื้อหาดังต่อไปนี้

  1. Store: ออฟฟิศของพนักงาน
  2. Provider: เป็นตัวกลางที่ทำให้ React ทำงานร่วมกับ Redux ได้
  3. State: เป็นข้อมูลกลางที่มีผลต่อการ render หน้าจอ ถูกเก็บอยู่ใน Store ทั้งหมด
  4. Reducer: พนักงานแบงค์ผู้ทำหน้าที่ในการรับคำสั่ง (action) แล้วส่งข้อมูล state กลับไป
  5. Connect: เป็น messenger ที่ทำหน้าที่คอยเป็นเด็กส่งเอกสารนำ state จาก store มาส่งให้กับ component
  6. mapStateToProps: เป็น callback ที่ใช้สำหรับรับค่า state จาก Store จากนั้นนำมา map ใส่ props แล้วปล่อยให้ connect ฉีด props ตัวนั้นไปยัง component ปลายทาง