จับ Component มา “โด๊ปยา” โดยใช้ recompose กันเถอะ

Tananan Tangthanachaikul
TakeMeTour Engineering
4 min readJan 15, 2018

ที่ TakeMeTour นั้น Front-End เราจะใช้ React เป็นส่วนใหญ่ ซึ่งอย่างที่หลายๆ ท่านอาจจะทราบอยู่แล้ว React นั้นเป็นหนึ่งใน Front-End Framework ที่มี Library เสริมทัพเยอะมาก เช่น Redux ไว้ใช้ในการจัดการ State / styled-components ที่ไว้ทำ styling React component ให้ง่ายขึ้น

วันนี้ผมจะพาทุกคนไปรู้จักกับ recompose กันครับ เป็น Library อีกตัวที่น่าสนใจสำหรับการเขียน React แต่ก่อนจะแนะนำ recompose นั้น ผมจะขอปูพื้นฐานบางส่วนของภาษา JavaScript ในเรื่อง Higher Order function ครับ

Get high(?) with Higher Order function

Higher Order function คือ function ที่

  1. รับฟังก์ชันไปเป็น parameter หรือ
  2. return function ออกมา

ในกรณีแรกอย่างเช่น map() และ filter() ของ Array ใน JavaScript เป็น Higher Order function เพราะเรารับ function เข้าไปเป็น parameter ซึ่งหลายคนก็คงคุ้นเคยดี

ส่วน function ที่ return function ออกมาอาจจะไม่ค่อยคุ้นๆ กันหนัก ให้ลองดูตัวอย่าง

ขั้นแรกดู addBy ก่อน มันคือ function ที่รับพารามิเตอร์ a เข้าไป และมันจะ return function ออกไป (ในที่นี้คือ b => a + b ) ซึ่งตัวแปร a ทั้งสองอันนั้นก็จะเป็นอันเดียวกัน ทำให้จุดประสงค์ของฟังก์ชันที่ได้มาคือ “รับค่า b เข้าไป และบวกเพิ่มไป a”

นั่นทำให้ลองมาดูที่ addByFive ที่มีค่าเป็น addBy(5)

addBy(5) จะเป็นฟังก์ชัน b => 5 + b (เพราะ a เป็น 5)

นั่นทำให้ addByFive จะกลายเป็นฟังก์ชันที่รับค่าอะไรมาก็ตาม บวกเพิ่มไปอีก 5 addByFive(7) จึงได้ 12

ในกรณี addByTen เช่นกัน

ซึ่ง higher order function จากกรณีที่สองนั้นนับว่า advance เอาการครับ ต้องศึกษา functional programming กันอย่างหนักหน่วงเลยทีเดียวถึงจะมีความเข้าถึงขั้นหนักมากครับ

ในโลกของ React ก็มี Higher Order Component!

ถ้าว่า Higher Order function ชวน high ไม่พอแล้ว ในโลกของ React เองก็มี Higher Order component เหมือนกัน

ว่าแต่แล้วนิยามมันคืออะไรล่ะ…ลองคิดดูครับว่า ถ้าเราบอกว่า Higher Order Function คือ function ที่ return function แล้ว Higher Order Component จะเป็น…

แอ่นแอ๊นนนนนน

ใช่แล้วครับ Higher Order Component = ฟังก์ชันที่รับ Component มาและ return Component ใหม่ไป

ซึ่งหารู้หรือไม่ว่า จริงๆ แล้วเราอาจจะเคยใช้ Higher Order Component มานานนมแล้วโดยไม่รู้ตัว

นั่นคือ

ใช่แล้วครับ Connect ของ Redux นั่นเอง ที่หลายๆ คนจะได้ใช้บ่อยหากได้จับ Redux ลองดูความสามารถที่มันทำครับ สิ่งที่ฟังก์ชัน connect ทำนั้นก็คือรับ Component มาเป็น parameter และมันก็จะ return Component ที่เชื่อมข้อมูลจาก Redux store สู่ Component ให้เรียบร้อย

ดังนั้นจริงๆ แล้วในโลกของ React การทำ Higher Order Component ผมจะมองว่าเหมือนการ “โด๊ปยา” ให้ Component สุดบ้านของเรามีพละกำลังวังชามากขึ้น

และเจ้า recompose นั้นก็จะเรียกได้ว่าเป็น “ร้านขายยาโด๊ปนานาชนิด” เลยก็ว่าได้

Recompose: ร้านขายยาโด๊ปหลากชนิด

Recompose เป็น Library ที่รวมเอา Higher Order component (จะขอย่อว่า HOC นะครับ) สาระพัดประโยชน์มาเสริมพลังให้กับ Component ของเราครับ

เพื่อให้เข้าใจว่ามันช่วยเสริมพลังยังไง ขอเริ่มจากตัวอย่างสุดบ้านๆ อย่างเช่น ถ้าเราจะทำ component ที่เป็น counter สามารถกดเพิ่ม/ลดตัวเลขได้ เราก็มักจะทำแบบนี้

เอาค่าของ counter เก็บใน state แล้ว mutate state ผ่าน setState ปกติ ชาว React ก็จะเข้าใจดี ไม่ติดขัดอะไร

แต่ถ้าเราลองใช้ Recompose ดูละ…Recompose มี HOC ตัวนึงชื่อว่า withState() ให้ใช้ครับ

ลองดูความคูลของมันดูครับ

สิ่งที่เราทำคือ

  1. เขียนฟังก์ชัน enhance (ผมจะชอบเรียกว่าฟังก์ชันโด๊ปยา)
  2. เอาฟังก์ชัน enhance ไปใช้กับ Component บ้านๆ ของเรา ผลที่ได้จะเป็น Component พร้อมการโด๊ปยาตามฟังก์ชัน enhance ที่จัดสรรมา

ซึ่งสิ่งที่ withState ทำนั้น ก็คือมันจะสร้าง state ขึ้นมาให้ โดยเราระบุชื่อ state รวมถึงค่าเริ่มต้น และฟังก์ชันที่จะใช้ mutate ค่าของ state นั้นให้ และทั้งหมดจะถูกนำไปส่งให้ props ของ Component ที่จะโด๊ปยาครับ

ถ้าสังเกต จะเห็นว่าตัว component ที่ส่งให้ enhance ฟังก์ชันของเรานั้นเป็น functional component ที่ใช้ state ไม่ได้ แต่ผลลัพธ์ที่ได้นั้นเสมือนว่ามี state อยู่ไม่มีผิดเลย!

หรือลองมาดูอีกปัญหาสามัญประจำบ้านที่เราจะชอบเจอกันครับ เวลาเราต้องทำ async loading เช่น รอข้อมูลจาก API เราก็จะต้องแสดง Loading component หรืออะไรสักอย่างให้เห็นว่ากำลังโหลดอยู่ ถ้าเป็นท่าเดิมๆ ก็น่าจะประมาณนี้

ซึ่งแน่นอนว่า ถ้ามีกี่จุดที่ต้องรอ ก็ต้องสอยตามไปยัดมันทุกจุด แต่ Recompose เรามี branch ให้ใช้ครับ!

หน้าที่ของ branch คือ จะให้เราเขียนฟังก์ชันที่จะไว้ test จาก props หากเป็น true ก็จะหยิบ HOC ใน parameter ถัดไปมาใช้ ถ้า false ก็จะหยิบอันถัดไปมาใช้

ดังนั้นเราจะสามารถ reuse ตัว enhance อันนี้ได้ ขอแค่ให้ Component นั้นๆ มันส่ง props loading และ manage state ของการ loading ถูก ก็จบ เรื่องการ render ตัว branch ของ recompose จะจัดการให้

แต่ในโลกจริง เรามักไม่ได้ใช้ HOC ตัวเดียวครับ Component นึงอาจจะมี state ให้ต้องจัดการเยอะแยะเต็มไปหมด ดังนั้นจึงมีฟังก์ชันที่เรียกว่า compose ที่จะรวมทุก HOC เข้ามาเป็นอันเดียวกันเลยครับ

Recompose in Action

เพื่อให้เห็นภาพว่าแนวคิดการเขียน React โดยใช้ Recompose เป็นยังไง ผมจะลองยกตัวอย่างให้ดูครับ

สมมติผมจะทำหน้าที่จะแสดงรายชื่อของผู้สมัครเข้าร่วมค่ายแห่งหนึ่ง โดยค่ายนี้มีสี่สาขาด้วยกัน ก็คือ Programming, Content, Design, Marketing โดยจะแสดงออกเป็นตารางแยก 4 อัน โดยที่

  • ตารางใช้ Component ชื่อ <CamperTable /> มี props ชื่อ lists รับ array ของผู้สมัคร
  • ดึงรายชื่อจาก API และนำไปเก็บไว้ที่ store ของ redux

ขั้นแรกสุด ผมจะสร้าง Component เพื่อแสดงผลขึ้นมาก่อน เรื่องของ Logic จะไปแปะไว้ที่หลัง

วิธีอาจจะดูไม่ reusable พอสมควร โอเค เดี๋ยวค่อยว่ากัน

ทีนี้ต่อมาผมก็จะเชื่อมกับ redux state แล้ว ก็จะใช้ฟังก์ชัน connect คู่กับ compose ไปเลยในตัว เพราะยังไง HOC ก็จะไม่จบแค่ตัวเดียวแน่นอน

ต่อมาเราต้องทำการยิง API และเปลี่ยนค่าจาก redux ซึ่งสมมติว่าผมทำทุกอย่างครบผ่าน action ของ redux ชื่อ loadCampers() แต่จังหวะที่เราจะต้องเรียกใช้ loadCampers() นั้นก็ต้องไป Lifecycle hook ที่ componentDidMount() แต่ว่า Campers component เราเป็น stateless component นี่หนา แล้วจะทำ lifecycle hook ยังไง

Recompose ดันเทพครับ มันมี lifecycle() HOC ให้ใช้ด้วย หน้าที่ของมันก็จะทำ lifecycle hook ของ React ให้เราเลย

ต่อมาแน่นอนว่าก็จะมีจังหวะที่ข้อมูลจาก API จะยังไม่มา ก็ควรจะต้องแสดงการ loading จึงเลือกใช้ branch ตามที่เคยโชว์ไปจากด้านบนก่อนหน้านี้

ถ้าดูตรง connect ผมเพิ่ม props loading ที่ดึงมาจาก redux state ดังนั้น branch ก็จะเห็น props ชื่อ loading (compose จะรัน HOC จากบนลงล่าง) ซึ่ง props จะโดนจัดการเปลี่ยนจาก true เป็น false เมื่อโหลดข้อมูลจาก API เสร็จ และเอาข้อมูลเข้า store

ถ้าดูทั้งหมดนี้แล้ว ก็ดูดีนะ ดูจะใช้ได้แล้ว แต่ผมรู้สึกว่าตัว component ยังวุ่นวายพอตัว ยังคงต้องมีการ filter เองตอน render แต่น่าจะดีกว่านะถ้าเราเตรียมแค่ array ธรรมดาให้พร้อมเลย ไม่ต้องไปทำอะไรต่อ

ขอนำเสนอ HOC ชื่อว่า withProps() ครับ

withProps ทำงานตามชื่อครับ คือผลิต props เพิ่มมาให้ ซึ่งสามารถเพิ่มตรงๆ เลยก็ได้ หรือในกรณีนี้ สามารถเอา props จากด้านบนมาเพิ่มต่อได้ ในเคสนี้ผมเอา campers props มา filter เรียงสาขา ทำให้ส่วน render ก็จะดูโล่งๆ ขึ้น ซึ่งยอม trade-off กับความยุ่งเหยิงที่ HOC แทน

ไปๆ มาๆ ก็มีคน request feature เพิ่มว่า อยากให้มันมี filter กดแสดงเฉพาะบางสาขาได้ งานนี้ withState ก็จะมาเป็นพระเอกของเราละ

ก็คือจะมี state displayRole เพิ่มมาที่จะบอกว่าตอนนี้จะแสดงผลตารางไหน และมีปุ่มกดสี่ปุ่มเพิ่มมา โดย bind action ให้เรียกฟังก์ชัน setDisplayRole ที่จะ mutate state ของ displayRole โดยตรง

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

ดูตรงส่วน mapProps ผมสามารถดึง state ของ displayRole มาได้จาก props เพราะอย่าลืมว่า recompose ทุกอันแม้จะทำหน้าที่เป็น state แต่มันก็จะถูกส่งมาในนามของ props แทนครับ

ผลลัพธ์สุดท้ายก็จะตามโค้ดด้านบนนี้เลย

ทำไมถึงติดใจ

ในระยะหลังที่ TakeMeTour เองเอา recompose มาใช้กับโปรเจคที่ขึ้นใหม่หลายๆ ตัว ผมใช้แล้วก็ติดใจมากๆ เพราะว่า

  • เลิกเขียน Class Component เพราะ Recompose จะช่วยให้เราเขียน functional component แทน แต่มีความสามารถเทียบเท่ากับ Class Component ได้เลย
  • แยกส่วน Logic กับส่วน Render ขาดจากกันได้ง่ายขึ้น (Logic อยู่ใน enhance, Render อยู่กับ functional component)
  • withProps ช่วยให้เรา simplified ส่วนของ render มากขึ้น เช่น onClick มา call function เดียวแทน แทนที่แต่ก่อนเราอาจจะยัดฟังก์ชันมหาศาลในนั้น
  • บาง component สามารถแยก pure functional component กับ enhanced component ได้ (export อันก่อน enhance และหลัง enhance) ทำให้เราเอา component มา reuse ใช้ที่อื่นได้มากขึ้น
  • และแน่นอนว่าเราสามารถเขียน HOC เพื่อประโยชน์หลายๆ อย่างได้ เช่น HOC สำหรับการทำ auth, HOC สำหรับการ render loading state และอื่นๆ ตามแต่จะประยุกต์ได้

แต่จุดปวดหัวก็มีเหมือนกัน

  • จุดที่น่าจะเป็น issue สุดก็น่าจะเป็น performance เพราะการ apply HOC ก็คือการมี component ยัดขึ้นมาอีกชั้น performance ก็อาจจะแย่ลงกว่าท่าปกติ
  • Document อ่านโคตรยากมากๆๆๆๆๆๆๆ กว่าจะทำอะไรได้ทีก็ปวดหัวที (อ่านยากระดับที่ว่าเหมือนเอา library มากางให้อ่านกันเลยทีเดียว)

หากสนใจศึกษาเพิ่มเติม สามารถลองดูได้ที่ repo หลักเลยครับ

หรือทั้งนี้ทั้งนั้นมีอีกบล็อกนึงที่เขียนไว้ดีเหมือนกัน

สำหรับบล็อกนี้ก็เป็นบล็อกแรกที่เขียนให้ TakeMeTour Engineering เหมือนกัน ถ้ามีข้อแนะนำอะไร สามารถติชมได้ครับ :)

ช่วงโฆษณา

TakeMeTour สตาร์ทอัพการท่องเที่ยวสัญชาติไทยแท้ๆ กำลังเปิดรับสมัคร Developer เพิ่มเติมอยู่ ในตำแหน่งต่อไปนี้

  • Senior/Junior JavaScript Engineer (Front-end/Back-end/Full-stack)
  • Junior Data/ML Engineer
  • Engineering Internship

Stack ที่เราใช้คร่าวๆ เป็นดังนี้

  • Front-End: React/Redux/Next.js/Expo
  • Back-End: Node.js/Express (with micro-service architecture)/Mongoose/Elastic Search/Redis

สำหรับการทำงานและสวัสดิการของเรานั้นประกอบด้วย

  • เวลาเข้าออก Flexible เข้าออกกี่โมงก็ได้ เราเน้นที่ผลงาน ไม่เน้นที่เวลาในการทำงาน
  • การแต่งตัวสบายๆ ใส่ชุดอะไรมาทำงานก็ได้ ขอแค่ให้เหมาะสม
  • ออฟฟิศเดินทางสะดวก ติด BTS และ Airport Rail Link สถานีพญาไท
  • วันหยุด 30 วันต่อปี
  • มี Google Home Mini เหงาหงอยรอคนมาคุยด้วย (ได้เหรอ)
  • รวมถึงยังมีบอร์ดเกมฝุ่นเกาะรอคนมาเล่นด้วยอยู่
  • และมี PS4 รอให้คนมาโค่นแชมป์ FIFA จาก CEO อยู่ด้วย

สำหรับผู้ที่สนใจ สามารถส่งอีเมล์แนบ Resume, Github เข้ามาได้ที่ WantToWork@takemetour.com ได้เลยครับ

--

--

Tananan Tangthanachaikul
TakeMeTour Engineering

Senior Full-Stack Developer@TakeMeTour - A man who passionately craft software