ทำไม React ของคุณถึงยังไม่ Hooks

Isoon PH
LifeatRentSpree
Published in
4 min readFeb 3, 2021
Photo by Mael BALLAND on Unsplash

เป็นเวลากว่า 2 ปี แล้วที่ React hooks ถูกปล่อยออกมาให้ใช้งานกันใน React เวอร์ชั่น 16.8 แต่เชื่อว่าหลาย project หรือ dev หลายๆท่านยังจำเป็นต้องเขียน และพัฒนา React ด้วย code base ที่ถูกสร้างไว้ก่อนที่ React hooks จะเข้ามา

บทความนี้เราจะมาย่อยความเป็นมา และปัญหาของ pattern Higher-order Component (HOC) ที่ถูกใช้กันอย่างแพร่หลาย ก่อนที่ hooks จะถูกปล่อยกันมาให้ใช้งานกันอย่างทุกวันนี้

หวังว่านี่จะเป็นจุดที่เราทุกคนลุกขึ้นมา refactor code base ให้ใช้ hooks เป็น code base แทนนะครับ

ขอย้อนกลับไปอีกนิดถึงเรื่อง React original concept ที่ถูกระบุไว้ที่หน้าแรกของเว็ปไซต์ React เอง ว่ามีอะไรที่ควรระลึกไว้ เพื่อให้ code base สามารถ maintain ต่อไปได้เรื่อยๆ ถึงแม้ React จะอัพเวอร์ชั่นใหม่ๆ

React original concept

  • Declarative — เน้นไปที่การระบุ(declare) การทำงานที่เราต้องการ โดยสนใจแค่ผลลัพธ์ที่ได้ โดยการทำแบบนี้จะทำให้ส่วนที่มองเห็น(UI) กับ program logic ง่ายต่อการทำอ่าน เช่น ปุ่มกด(UI) เรียกฟังก์ชั่นlogin ได้ผลลัพธ์เป็นการ login
  • Component-Based — สร้างสิ่งต่างๆเป็น component ที่จะรับผิดชอบการจัดการภายในของตัวเองไม่ว่าจะเป็น state, event หรือ data flow แล้วจึงนำมาประกอบกัน (compose) สร้างเป็น UI ที่ซับซ้อน
  • Learn Once, Write Anywhere — React มีทั้ง web app และ React-native ที่เป็น mobile app

นอกจาก Declarative จะเป็น concept ของ React แล้ว Declarative ยังเป็น concept หลักของ functional programming อีกเช่นกัน (แนบ functional programming) React จึงถูกเรียกว่าเป็น library ที่ใช้ functional programming เป็นหลักในการพัฒนา

และ Component-Based ที่สามารถเข้ามาแก้ไขปัญหาในการเขียน UI ทำให้เพิ่ม reusability ได้มหาศาล หรือ Life Cycle API ที่เข้ามาช่วยให้ผู้ใช้กับ web application เกิด interaction ได้มากขึ้นเช่นกัน

ถึงแม้ Component-Based จะช่วยให้สามารถ reuse ในส่วนของ UI ได้แต่การ reuse logic ที่จำเป็นต้องมีการใช้ React API ต่างๆ ไม่ว่าจะเป็น State Props API หรือ Lifecycle API เอง ยังเป็นเรื่องยากและยังมีปัญหาตามมาค่อนข้างเยอะเลยทีเดียว

ด้วยการปรับ นำ Higher-order function (HOF) มาใช้กับ React Component จนเกิดเป็น Higher-order Component (HOC) ด้วย concept การ compose logic ต่างๆ เข้าด้วยกันจนสร้างเป็น “pipeline” อ่าน HOF ที่นี่

Higher-order Component Adoption

โจทย์ที่เกิดขึ้นในตอนต้นคือ เมื่อต้องการใช้งาน React API จำเป็นจะต้องเรียกใช้ใน class component เท่านั้น

ทำให้เราต้องไปสร้าง class component เพื่อดึง React API มาใช้ และเมื่อเราได้ Logic ที่เราต้องการ reuse

จึงทำการ render UI หรือ component อื่นที่เราต้องการออกไป

ลักษณะดังกล่าวจะเป็นการห่อ(wrap) component ของเราด้วย component ที่เราอยาก reuse ไว้

แบบนี้

class BaseContent extends React.Component {
constructor {
// ...
}
// ...
render() {
<Container>
<Topic authStatus={this.props.status} />
</Container>
}
}
export withAuthStatus(BaseContent)

ซึ่งก็ถือว่าดู make sense เพราะการเอา component นึงมาผ่านชุด logic (withAuthStatus) ที่เราต้องการ

และยังสามารถส่งข้อมูลผ่านไปที่ component ได้ผ่าน Props API ทำให้เราสามารถนำชุด logic ไปใช้ในจุดอื่นๆ ได้

และ pattern นี้ก็ยังดันตรงตาม concept functional programming ด้วยการนำ Higher-order Function เข้ามาใช้

Higher-order Function คืออะไร

แถมให้นิดหน่อยใน functional programming, higher-order function(HoF) เป็นหลักการสำคัญในการเขียนในเชิง functional programming

HOF คือการที่ function รับ หรือรีเทิร์น function ออกไป และเมื่อสิ่งที่ return ไปเป็น function และ function นั้นก็สามารถทำแบบเดิมซ้ำไปได้เรื่อยๆ

ทำให้หลักการสำคัญของ HoF คือการที่สามารถ compose function ต่างๆเข้าด้วยกันได้ ทำให้ตัวมันเองสามารถรับค่า คืนค่า และคืนค่า เป็น function แบบนี้ไปได้เรื่อยๆ จนเกิดเป็น “pipeline”

เมื่อ HOC นำ HOF เข้ามาใช้ ดังนั้นตัว HOC จึงสามารถครอบ Component ไปกี่ชั้นก็ได้ ทำให้มี Library สำหรับจัดการการ compose มากมาย

ลองจินตนาการ Component ของเราเป็น ของขวัญที่เราจะซื้อให้เพื่อนในวันเกิด ที่เราต้องห่อ(wrap) ของขวัญด้วยกล่องหนึ่งชั้น และด้วยยุคโควิด เราเลยต้องส่งของขวัญผ่านไปรษณีย์ เราจึงต้องห่อ(wrap) มันอีกชั้นนึง สุดท้ายเพื่อนเราต้องเปิดกล่องของขวัญถึง 2 รอบด้วยกัน

ใช่แล้ว นั่นคือสิ่งที่เราจะเจอเมื่อทำการใช้ HOC pattern 2 รอบ แต่ถ้าเป็น 5 หรือ 6 รอบที่เราจะเป็นต้องห่อ(wrap) component นั้นล่ะ กล่องของขวัญ หรือ component นั้นจะใหญ่ขึ้นกว่าในตอนที่เราซื้อมาในตอนแรกอย่างเห็นได้ชัดเลยทีเดียว

HOC problem

ปัญหาที่ตามมาจากการจำเป็นต้อง reuse logic ที่เยอะใน project ที่มีความซับซ้อนเยอะๆ คือ component จะใหญ่มาก เนื่องจากสาเหตุต่างๆ เช่น :

  • HOC คือ class ที่ดึงเอา React API ทั้งมาผ่านการ extends และเมื่อมันถูก compose เข้าด้วยกันหลายๆอัน มันจึงทำให้ component นั้น load นานมาก

— แนบ benchmark ของ functional component และ class component

ref: https://medium.com/missive-app/45-faster-react-functional-components-now-3509a668e69f
  • HOC ยังเป็น component ที่ return component ทำให้ทุกครั้งการ compose HOC จะเป็นการ render ซึ่งทำให้มี JSX tag มาครอบหลายชั้นตามจำนวนการ compose จนเกิดเป็นปัญหาที่เรียกว่า Wrapped hell

Wrapped Hell

Wrapped hell ถือว่าเป็นปัญหาอย่างมากในการ debug React component เพราะเมื่อ React component ถูก render จากการ compose HOC หลายๆชั้น จะทำให้ DOM ของเรามีหน้าตาแบบนี้

ref — https://medium.com/@jackyef/react-hooks-why-we-should-embrace-it-86e408663ad6

จะเห็นว่า DOM ของเราจะมี tag ชื่อแปลกๆเต็มไปหมด ซึ่งทำให้ยากต่อการ track error หรือ data passing ค่อนข้างมาก

แต่ปัญหาจริงๆของ HOC ที่ผู้เขียนรู้สึกคือ การที่ HOC ดันไปดึงเอาความสามารถของ React API มาทั้งหมด ซึ่งบาง component อาจจะใช้แค่บาง API จึงทำให้เกิด Overhead ในการ load component ดังที่เห็นใน benchmark ตอนต้น

รวมทั้ง wrapped hell ไปลด readability ให้ code ของเราทำให้ยากต่อการ maintain ในภายหลัง

นี่เองทำให้การเข้ามาของ React hooks สามารถตอบปัญหาต่างๆ ที่กล่าวมาตั้งแต่ต้นได้ตรงจุด และยังคงความเป็น functional programming ไว้ครบถ้วน

Solution with React hooks

เนื่องจาก class component จะถูก inherit React API มาเป็นปกติอยู่แล้ว ดังนั้น hooks จึงเข้าไปเพิ่มความสามารถให้กับ stateless component ด้วย concept ตามชื่อง่ายๆว่า hooks หรือก็คือการดึง

ในที่นี้คือการดึงมาเพื่อใช้งาน โดยเราจะดึง React API อะไรก็ได้มาใช้ ทำให้เราสามารถเลือก และ optimize ได้ว่า component ของเราต้องการใช้ React API ที่จำเป็น ทำให้ component ของเราไม่มี overhead เกิดขึ้นเหมือน class component

Hooks มีกฏง่ายๆ ในการใช้งานอยู่แค่ 2 ข้อ คือ

  • ต้องถูกเรียกอยู่ที่ top-level — ในที่นี้หมายถึง ไม่อยู่ใน if-else, loop หรือ nested function เพื่อการันตีว่า hooks จะถูกเรียกทุกครั้งที่ component มีการ render
  • ต้องถูกเรียกภายใน React Functional Component เท่านั้น (แต่สามารถถูกเรียกใน function ที่เป็น custom hooks ที่จะพูดถึงข้างล่างนี้ได้)

อ่าน hooks เพิ่มเติม ที่นี่
อ่าน React Functional Component vs class component เพิ่มเติม ที่นี่

นอกจาก hooks จะไม่สร้าง overhead ให้กับ component แล้ว แต่สิ่งที่ hooks เข้ามาแทนที่ HOC ได้ก็คงต้องเป็นความสามารถในการ reuse logic ที่ไม่เกิด wrapped hell และเพิ่ม Readability ให้ code base ของเรานั่นเอง

Custom Hooks

ความดีงามของ hooks คือการที่ React ให้ความสามารถของ function ในการดึง(hook) React API อะไรมาใช้ก็ได้ จึงทำให้เราสามารถ ระบุและกำหนดการทำงานของ logic ที่สามารถใช้งาน React API ได้อย่างยืดหยุ่น

และแน่นอน custom hooks ไม่จำเป็นต้อง render component อะไรเพิ่มทั้งนั้น แต่เป็นการ เรียกมาใช้ใน React Functional Component ได้เลย

แบบนี้

const BaseContent = (props) => {
const { status } = useAuthStatus(props.user)
return (
<Container>
<Topic authStatus={status} />
</Container>
}
export default BaseContent

เทียบกับการสร้าง component BaseContent ที่สร้างไว้เป็น class component ในตอนแรก จะเห็นได้ว่าการใช้งาน AuthStatus ของ React hooks มีความ declarative มากกว่า HOC เนื่องจากเราได้ค่า status มาจากการเรียก useAuthStatus โดยระบุ user ลงไปได้ โดยไม่ต้องส่งค่าผ่าน Prop มาจาก HOC นั่นเอง

อ่าน custom hooks ต่อ ที่นี่

และเมื่อ logic ถูกแยกเป็น function ย่อยๆ ก็ทำให้ง่ายต่อการเขียน test อีกด้วย

conclusion

ถือได้ว่า React hooks เป็น concept สั้นๆ แต่มีอานุภาพมหาศาล ทั้งเพิ่มความง่ายในการ debug ลด overhead ของ component หรือเพิ่ม readability ให้ code base ของเราให้ง่ายต่อการ maintain

“ทำไม React ของคุณถึงยังไม่ hooks” หวังเป็นอย่างยิ่งว่าผู้อ่านจะมองเห็นถึงปัญหาที่เกิดขึ้นใน Higher-order Component และค่อยๆ เปลี่ยนกันมาใช้ hooks pattern เพื่อเพิ่มประสิทธิภาพให้กับ component ของเรา

ทั้งนี้ผู้เขียนยังอยากให้ผู้อ่านทุกท่านไปศึกษาข้อดี ข้อเสียและข้อจำกัดของ hooks เพิ่มเติมเพื่อเพิ่มประสิทธิภาพให้ component และ project ได้อย่างเต็มที่ — เพิ่มเติม react pattern ต่างๆ

หากมีตรงไหนอ่านแล้วงงหรือจุดไหนให้ข้อมูลผิดพลาด รบกวน comment ผู้เขียนขอยอมรับและแก้ไขเพื่อให้บทความนี้มีความสมบูรณ์ที่สุด

ขอบคุณครับ :)

--

--