React Design Patterns: Higher-Order Component

Ayuth Mangmesap
SCB TechX
Published in
4 min readMay 22, 2023
Cover photo by Pattaporn Lertmanka

ว่ากันอีกหนึ่งรูปแบบที่เป็นที่นิยมเมื่อกาลก่อนมาถึงปัจจุบันคือ Pattern ที่ชื่อว่า Higher-Order Component หรือ HOC

ใน Official Recat Document มีข้อความต่อนหนึ่งที่เขียนเกี่ยวกับ HOC ไว้ว่า

Concretely, a higher-order component is a function that takes a component and returns a new component.

กล่าวคือ Higher-order Component คือฟังก์ชันที่รับ Component เข้ามาและคืนไปเป็น Component ใหม่ ถ้ายังไม่เห็นภาพไม่เป็นไรมาลองดูตัวอย่างกันครับ

React ในยุคก่อน Class คือทุกอย่าง

ก่อนหน้านี้ที่ React Hooks จะถูกเสนอเข้ามาใน React RFC การที่เราจะเรียกใช้ state หรือ life cycle ของ React Component ได้นั้นเราจะเป็นที่จะต้องประกาศ class และ extends React.Component เสมอ

import React from 'react'

class App extends React.Component {
render() {
return <h1>Hi there!</h2>;
}
}

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

คาดว่าต้นกำเนิดของ Higher-Order Components มาจากแนวคิด Higher-Order Functions ที่รับค่าของ Function หนึ่งมาและคืน Function ใหม่ออกมา อ่านมาแล้วอาจงงแต่คิดว่าเราคงเห็นและใช้กันบ่อย เช่น map , reduce, filter

🪄 ชูการ์ ชูการ์ รูน คลาสของเธอฉันขอนะ

ทว่าข้อจำกัดเหล่านี้ก็มิได้ทำให้ Developer มีความพยายามน้อยลงในการที่จะแยกส่วนของContainer และ Presentational Component เพื่อแยกส่วนของโค้ดให้กลับมาใช้ใหม่ได้ง่ายขึ้น

function withSomething(Component) {
return class extends React.Component {
/* ... */
};
}

อาจงงกัน งั้นเราลองมาสร้างสักฟังก์ชันหนึ่งเพื่อเก็บค่า mouse position ของ user ที่ชื่อ withMousePosition กัน

import React from "react";

function withMousePosition(Component) {
return class extends React.Component {
state = {
x: null,
y: null
};
updateMousePosition = (event) => {
this.setState({ x: event.clientX, y: event.clientY });
};
componentDidMount() {
window.addEventListener("mousemove", this.updateMousePosition);
}
componentWillUnmount() {
window.removeEventListener("mousemove", this.updateMousePosition);
}
render() {
return <Component {...this.props} {...this.state} />;
}
};
}

function App(props) {
const { x, y } = props
return (
<div className="App">
<p>
Current mouse position: x: {x}, y: {y}
</p>
</div>
)
}

export default withMousePosition(App);

โค้ดด้านบนทำหน้าที่เพิ่มความสามารถให้แก่ Component ซึ่งความสามารถนั้นคือการดึงค่า mouse position เมื่อไรก็ตามที่ user ของเราขยับเมาส์

ประกอบ Higher-Order Component เข้าด้วยกัน

Photo by Mel Poole

เหล่านักพัฒนาแฮปปี้ชีวิตดี๊ด๊าเพราะ HOC ทำให้โค้ดของเขานำกลับมาใช้ใหม่ได้ง่ายมากขึ้นรวมถึงการบำรุงรักษาส่วนของโค้ดเหล่านั้นด้วย จากเดิมที่ Component หนึ่งมีทุกอย่าง เราสามารถค่อย ๆ แยกเรื่องที่ไม่เกี่ยวข้องออกจากกันและรวมเรื่องที่เกี่ยวข้องเข้ามาไว้ด้วยกัน เช่น เรามี Component ที่ต้องการข้อมูล position ของ mouse ของผู้ใช้งาน ทางฝั่ง consumer ก็สามารถใช้ HOC ครอบได้ทันที เช่น

withMousePosition(Component)

เมื่อวันเวลาผ่านไป Component ต้องการข้อมูลของ user ที่กำลังจะใช้งานด้วยก็สามารถครอบทับอีกทีเพื่อเพิ่มพลังให้แก่ Component นั้นด้วยฟังก์ชัน withUserProfile ที่มีสิ

withUserProfile(withMousePosition(Component))

ยังไม่พอ! มีข้อมูล user แล้วแต่ยังขาด Notification ที่ต้องการแสดงผลไปให้แก่ผู้ใช้ เราก็สร้างอีกฟังก์ชันหนึ่งขึ้นมาคือ withNotification

withNotification(withUserProfile(withMousePosition(Component)))

เอาละพอแค่นี้ คงจะเห็นภาพว่าเราสามารถเพิ่มความสามารถ(enhance)ใด ๆ ให้แก่ Component เพียงแค่ครอบ HOC ของเราเข้าไป แต่เมื่อไรก็ตามที่เรายิ่งเพิ่มความสามารถมากขึ้นโค้ดในส่วนของ Component จะยาวขึ้น

/* ... */

let BaseComponent = withMousePosition(BaseComponent)
EnhancedComponent = withUserProfile(EnhancedComponent)
EnhancedComponent = withNotification(EnhancedComponent)
return EnhancedComponent;

recompose ฉันเลือกนาย!

Library ยอดนิยมสมัยที่ Higher-Order Comonent เป็นที่นิยมกันสมัยนั้นชื่อ recompose ซึ่งมีฟังก์ชันน่ารัก ๆ หลายตัวทำให้เราเขียนโค้ดไปฟินไป ถ้าไม่เชื่อลองอ่านใน Official API Document ของ recompose ได้

ไหนไหนก็พูดถึงศิษย์พี่อย่าง recompose แล้ว เราลองเอา helper function เข้ามาช่วยให้อ่านง่ายขึ้นนิดหนึ่งฟังก์ชันนั้นชื่อ compose ที่จะทำหน้าที่

import { compose } from "recompose"

/* ... */
const enhance = compose(
withMousePosition,
withUserProfile,
withNotification,
);

const EnhancedComponent = enhance(Component)

เราเรียกว่า enhancer โดย props จะลำดับส่งมาจากบนลงล่าง

Downsides of HOC

เราย้อนเวลามองภาพในอดีตกันแล้วทีนี้เรามาลองดูอีกมุมที่ควรจะระวังกันไว้บ้างกันครับ เมื่อเรา compose หลาย function เข้ามามาด้วยกัน ปัญหาที่อาจตามมาอาจเกิดได้หลายประการ เช่น

1. Props Collisions 🚘💥🚙

เมื่อเราห่อ component หลายชั้นเข้าอีกสิ่งหนึ่งที่อาจจะเจอได้คือปัญหาการชนกันของ props

Component หนึ่งที่ได้รับ props มายังไม่มีปัญหาแต่จะมีปัญหาก็ต่อเมื่อ props นั้นชื่อซ้ำกัน ในภาพด้านบนคือมี withBlogPosts , withUserProfile และ withSubscription

มีบล็อกที่น่าสนใจที่เขียนถึงวิธีการแก้ปัญหาโดยการกำหนด namespace ก่อนจะส่งไปให้ฟังก์ชัน ใครสนใจลองไปอ่านได้ที่บล็อกด้านล่างครับ 👇

2. Implicit Props

An illustration of implicit props; the component itself doesn’t know where prop is coming from

อีกข้อควรระวังก็เกี่ยวกับ props เช่นกัน เอาละเราลองมาอ่านโค้ดด้านล่างกันครับ 👇

const enhance = compose(
connect,
withNotification,
withUserProfile,
withState,
withStateHandlers,
withProps
)
const EnhancedComponent = enhance(Component)

ในครั้งแรกที่ดูมองดูเป็นระเบียบมากเพราะแยกเรื่องแต่ละเรื่องออกจากกัน หลังจากนั้นนำแต่ละเรื่องมาเรียงร้อยเป็นและประกบเข้ากับ Component

ทว่าวันดีคืนดีหนึ่งใน function with... ได้เกิดเหตุการณ์ที่มีการเปลี่ยนชื่อของ props ที่ส่งลงมาให้กับ Component จากนั้นทำการรันเทสหลังจากเปลี่ยนกลับไม่เจอข้อผิดพลาดใด กระทั่งมีลูกค้าบอกว่าใช้แอพไม่ได้! เมื่อนั้นละครับความฉิบหายจะมาเยือนทันที

ลองคิดถึงกรณีที่เกิดบัคในโค้ดด้านบน การ debug ไม่ใช่เรื่องง่ายเลยครับ เราต้องตามลงมาดูแต่ละฟังก์ชันว่าแต่ละฟังก์ชันนั้นคืนค่าอะไรออกมาและชื่อตัวแปรต่างทั้งหลายโดนเขียนทับ(override)ตอนไหนและเมื่อไรมันจะยุ่งยากยากและเหนื่อยเกินกว่าที่จำเป็น ถ้าเป็นแอพพลิเคชันเล็ก cost ของการค้นหาและ debug อาจจะไม่เท่าไรแต่ถ้าเป็นแอพพลิเคชันระดับกลางถึงใหญ่ขึ้นไปเชื่อว่าคนที่ต้องงมหา bug จะเจ็บปวดไม่น้อยที

3. Re-Rendering The Whole Tree

อีกสถานการณ์หนึ่งที่อาจเกิดขึ้นได้คือการ re-render ทั้งหมดของทั้ง Tree เมื่อ props เปลี่ยนไป

ในขณะเดียวกันที EnhancedComponent มีข้อมูลที่เราต้องการทุกอย่างและถูกนำไป render เป็น list หรือ list item

เมื่อ props เปลี่ยน สิ่งที่เกิดขึ้นคือทั้ง tree ภายใต้ list นั้นจะถูก re-render ทั้งหมด ถ้านักพัฒนาไม่ได้คำนึงถึงจุดนี้สิ่งนี้อาจส่งผลต่อประสิทธิภาพการใช้งานของแอพพลิเคชันแก่ user ของเราได้ และการแก้ปัญหาในลำดับถัดมาคือการ Performance Optimization ของแอพ

Good Bye HOCs; Welcome Hooks

Photo by Jonathan Kemper

เมื่อ React Hooks ได้เข้ามา ลักษณะการเขียนและการสร้าง Component ก็ทำได้ง่ายมากยิ่งขึ้น ไม่ต้องสร้าง class … extends React.Component ให้ยุ่งยาก ทำให้โค้ดที่นักพัฒนาเขียนนำเอาความสามารถของ state และ life cycles ได้ง่ายยิ่งขึ้น

เพื่อให้เห็นภาพชัดขึ้นเราลองมาเปลี่ยน HOC ของฟังก์ชัน withMousePosition โดยใช้ hooks ดูจะได้

import React from "react";

function useMousePosition() {
const [mousePosition, setMousePosition] = React.useState({
x: null,
y: null
});
React.useEffect(() => {
const updateMousePosition = (ev) => {
setMousePosition({ x: ev.clientX, y: ev.clientY });
};
window.addEventListener("mousemove", updateMousePosition);
return () => {
window.removeEventListener("mousemove", updateMousePosition);
};
}, []);
return mousePosition;
}
/* ... */

ส่วนวิธีการนำไปใช้งานค่อนข้างตรงไปตรงมา

import useMousePosition from '...'

function SomeComponent() {
const mousePosition = useMousePosition()

/* ... */
}

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

Looking forward to HOC

https://npm-stat.com/charts.html?package=recompose&from=2015-05-02&to=2023-05-02

หากเรามองสถิติการดาวน์โหลดย้อนหลังของ library ยอดนิยมอย่าง recompose จะเห็นว่าสัดส่วนการดาวน์โหลดมีแนวโน้มลดลงอย่างเห็นได้ชัดและไม่มีทีท่าว่าจะกลับมาจึงเป็นข้อสรุปว่า Pattern นี้ไม่เป็นที่นิยมแล้ว

อย่างไรก็ตามถึงแม้ HOC อาจจะไม่ค่อยเป็นที่นิยมและเห็นอยู่ในปัจจุบันแต่ก็มิใช่ว่าจะไร้ประโยชน์เพียงเพราะว่าไม่ได้รับความนิยมแล้ว หากใครเคยเห็นฟังก์ชันเหล่านี้ผ่านตา เช่น React.forwardRef, React.memo ฟังก์ชันเหล่านี้คือ HOC ฟังก์ชันหนึ่งครับ การนำมาใช้งานนั้นเรียบง่าย ทรงพลัง และตรงไปตรงมา

เก็บ pattern นี้พกใส่กระเป๋าและเอามาใช้ในยามจำเป็นก็ดีนะครับ …

References

--

--

Ayuth Mangmesap
SCB TechX

A developer who runs everyday because he eats a lot