ไทย

สำรวจรูปแบบ React Context Provider ขั้นสูงเพื่อจัดการ state, เพิ่มประสิทธิภาพ และป้องกันการ re-render ที่ไม่จำเป็นในแอปพลิเคชันของคุณ

รูปแบบ React Context Provider: การเพิ่มประสิทธิภาพและหลีกเลี่ยงปัญหาการ Re-render

React Context API เป็นเครื่องมือที่ทรงพลังสำหรับจัดการ state ส่วนกลาง (global state) ในแอปพลิเคชันของคุณ ช่วยให้คุณสามารถแบ่งปันข้อมูลระหว่างคอมโพเนนต์ต่างๆ โดยไม่ต้องส่ง props ผ่านทุกระดับด้วยตนเอง อย่างไรก็ตาม การใช้ Context อย่างไม่ถูกต้องอาจนำไปสู่ปัญหาด้านประสิทธิภาพ โดยเฉพาะการ re-render ที่ไม่จำเป็น บทความนี้จะสำรวจรูปแบบต่างๆ ของ Context Provider ที่ช่วยให้คุณเพิ่มประสิทธิภาพและหลีกเลี่ยงข้อผิดพลาดเหล่านี้

ทำความเข้าใจปัญหา: การ Re-render ที่ไม่จำเป็น

โดยปกติแล้ว เมื่อค่าใน Context เปลี่ยนแปลง คอมโพเนนต์ทั้งหมดที่ใช้ (consume) Context นั้นจะ re-render ใหม่ แม้ว่าคอมโพเนนต์เหล่านั้นจะไม่ได้ขึ้นอยู่กับส่วนของ Context ที่เปลี่ยนแปลงไปก็ตาม สิ่งนี้อาจกลายเป็นคอขวดด้านประสิทธิภาพที่สำคัญ โดยเฉพาะในแอปพลิเคชันขนาดใหญ่และซับซ้อน ลองพิจารณาสถานการณ์ที่คุณมี Context ที่เก็บข้อมูลผู้ใช้ การตั้งค่าธีม และการตั้งค่าของแอปพลิเคชัน หากมีการเปลี่ยนแปลงเฉพาะการตั้งค่าธีม ตามหลักการแล้วควรมีเพียงคอมโพเนนต์ที่เกี่ยวข้องกับธีมเท่านั้นที่ควร re-render ไม่ใช่ทั้งแอปพลิเคชัน

เพื่อให้เห็นภาพ ลองจินตนาการถึงแอปพลิเคชันอีคอมเมิร์ซระดับโลกที่เข้าถึงได้ในหลายประเทศ หากการตั้งค่าสกุลเงินเปลี่ยนแปลง (ซึ่งจัดการภายใน Context) คุณคงไม่ต้องการให้แคตตาล็อกสินค้าทั้งหมด re-render ใหม่ มีเพียงส่วนแสดงราคาเท่านั้นที่ต้องอัปเดต

รูปแบบที่ 1: การทำ Memoization ให้กับค่าด้วย useMemo

วิธีที่ง่ายที่สุดในการป้องกันการ re-render ที่ไม่จำเป็นคือการทำ memoize ให้กับค่าของ Context โดยใช้ useMemo สิ่งนี้ช่วยให้แน่ใจว่าค่าของ Context จะเปลี่ยนแปลงก็ต่อเมื่อ dependencies ของมันเปลี่ยนแปลงเท่านั้น

ตัวอย่าง:

สมมติว่าเรามี `UserContext` ที่ให้ข้อมูลผู้ใช้และฟังก์ชันสำหรับอัปเดตโปรไฟล์ของผู้ใช้


import React, { createContext, useState, useMemo } from 'react';

const UserContext = createContext(null);

function UserProvider({ children }) {
  const [user, setUser] = useState({
    name: 'John Doe',
    email: 'john.doe@example.com',
    location: 'New York, USA'
  });

  const updateUser = (newUserData) => {
    setUser(prevState => ({ ...prevState, ...newUserData }));
  };

  const contextValue = useMemo(() => ({
    user,
    updateUser,
  }), [user, setUser]);

  return (
    
      {children}
    
  );
}

export { UserContext, UserProvider };

ในตัวอย่างนี้ useMemo ช่วยให้แน่ใจว่า `contextValue` จะเปลี่ยนแปลงก็ต่อเมื่อ state `user` หรือฟังก์ชัน `setUser` เปลี่ยนแปลงเท่านั้น หากไม่มีการเปลี่ยนแปลงใดๆ คอมโพเนนต์ที่ใช้ `UserContext` จะไม่ re-render

ข้อดี:

ข้อเสีย:

รูปแบบที่ 2: การแยกความรับผิดชอบด้วย Context หลายตัว

แนวทางที่ละเอียดขึ้นคือการแบ่ง Context ของคุณออกเป็น Context ย่อยๆ หลายตัว โดยแต่ละตัวรับผิดชอบ state เฉพาะส่วน ซึ่งจะช่วยลดขอบเขตของการ re-render และทำให้แน่ใจว่าคอมโพเนนต์จะ re-render ก็ต่อเมื่อข้อมูลที่ตนเองขึ้นอยู่ด้วยมีการเปลี่ยนแปลงเท่านั้น

ตัวอย่าง:

แทนที่จะใช้ `UserContext` เพียงตัวเดียว เราสามารถสร้าง context แยกสำหรับข้อมูลผู้ใช้และการตั้งค่าผู้ใช้


import React, { createContext, useState } from 'react';

const UserDataContext = createContext(null);
const UserPreferencesContext = createContext(null);

function UserDataProvider({ children }) {
  const [user, setUser] = useState({
    name: 'John Doe',
    email: 'john.doe@example.com',
    location: 'New York, USA'
  });

  const updateUser = (newUserData) => {
    setUser(prevState => ({ ...prevState, ...newUserData }));
  };

  return (
    
      {children}
    
  );
}

function UserPreferencesProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [language, setLanguage] = useState('en');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    
      {children}
    
  );
}

export { UserDataContext, UserDataProvider, UserPreferencesContext, UserPreferencesProvider };

ตอนนี้ คอมโพเนนต์ที่ต้องการเฉพาะข้อมูลผู้ใช้สามารถใช้ `UserDataContext` และคอมโพเนนต์ที่ต้องการเฉพาะการตั้งค่าธีมสามารถใช้ `UserPreferencesContext` ได้ การเปลี่ยนแปลงธีมจะไม่ทำให้คอมโพเนนต์ที่ใช้ `UserDataContext` re-render และในทางกลับกัน

ข้อดี:

ข้อเสีย:

รูปแบบที่ 3: ฟังก์ชัน Selector ด้วย Custom Hooks

รูปแบบนี้เกี่ยวข้องกับการสร้าง custom hooks ที่ดึงข้อมูลเฉพาะส่วนของค่าใน Context และจะ re-render ก็ต่อเมื่อส่วนเฉพาะนั้นๆ เปลี่ยนแปลง ซึ่งมีประโยชน์อย่างยิ่งเมื่อคุณมีค่า Context ขนาดใหญ่ที่มีหลาย property แต่คอมโพเนนต์ต้องการใช้เพียงไม่กี่ property

ตัวอย่าง:

โดยใช้ `UserContext` เดิม เราสามารถสร้าง custom hooks เพื่อเลือก property เฉพาะของผู้ใช้


import React, { useContext } from 'react';
import { UserContext } from './UserContext'; // สมมติว่า UserContext อยู่ในไฟล์ UserContext.js

function useUserName() {
  const { user } = useContext(UserContext);
  return user.name;
}

function useUserEmail() {
  const { user } = useContext(UserContext);
  return user.email;
}

export { useUserName, useUserEmail };

ตอนนี้ คอมโพเนนต์สามารถใช้ `useUserName` เพื่อ re-render เฉพาะเมื่อชื่อผู้ใช้เปลี่ยนแปลง และใช้ `useUserEmail` เพื่อ re-render เฉพาะเมื่ออีเมลของผู้ใช้เปลี่ยนแปลง การเปลี่ยนแปลง property อื่นๆ ของผู้ใช้ (เช่น ที่อยู่) จะไม่ทำให้เกิดการ re-render


import React from 'react';
import { useUserName, useUserEmail } from './UserHooks';

function UserProfile() {
  const name = useUserName();
  const email = useUserEmail();

  return (
    

Name: {name}

Email: {email}

); }

ข้อดี:

ข้อเสีย:

รูปแบบที่ 4: การทำ Memoization ให้กับคอมโพเนนต์ด้วย React.memo

React.memo เป็น higher-order component (HOC) ที่ทำ memoize ให้กับ functional component ซึ่งจะป้องกันไม่ให้คอมโพเนนต์ re-render หาก props ของมันไม่มีการเปลี่ยนแปลง คุณสามารถใช้สิ่งนี้ร่วมกับ Context เพื่อเพิ่มประสิทธิภาพให้ดียิ่งขึ้น

ตัวอย่าง:

สมมติว่าเรามีคอมโพเนนต์ที่แสดงชื่อผู้ใช้


import React, { useContext } from 'react';
import { UserContext } from './UserContext';

function UserName() {
  const { user } = useContext(UserContext);
  return 

Name: {user.name}

; } export default React.memo(UserName);

การครอบ `UserName` ด้วย `React.memo` จะทำให้มัน re-render ก็ต่อเมื่อ prop `user` (ที่ส่งผ่าน Context โดยปริยาย) เปลี่ยนแปลงเท่านั้น อย่างไรก็ตาม ในตัวอย่างง่ายๆ นี้ `React.memo` เพียงอย่างเดียวจะไม่สามารถป้องกันการ re-render ได้ เพราะอ็อบเจกต์ `user` ทั้งหมดจะยังคงถูกส่งมาเป็น prop เพื่อให้มันมีประสิทธิภาพอย่างแท้จริง คุณต้องใช้ร่วมกับฟังก์ชัน selector หรือ context ที่แยกจากกัน

ตัวอย่างที่มีประสิทธิภาพมากขึ้นคือการรวม `React.memo` เข้ากับฟังก์ชัน selector:


import React from 'react';
import { useUserName } from './UserHooks';

function UserName() {
  const name = useUserName();
  return 

Name: {name}

; } function areEqual(prevProps, nextProps) { // ฟังก์ชันเปรียบเทียบที่กำหนดเอง return prevProps.name === nextProps.name; } export default React.memo(UserName, areEqual);

ในที่นี้ `areEqual` เป็นฟังก์ชันเปรียบเทียบที่กำหนดเองซึ่งจะตรวจสอบว่า prop `name` มีการเปลี่ยนแปลงหรือไม่ หากไม่เปลี่ยนแปลง คอมโพเนนต์จะไม่ re-render

ข้อดี:

ข้อเสีย:

รูปแบบที่ 5: การรวม Context และ Reducers (useReducer)

การรวม Context กับ useReducer ช่วยให้คุณสามารถจัดการตรรกะของ state ที่ซับซ้อนและเพิ่มประสิทธิภาพการ re-render ได้ useReducer มีรูปแบบการจัดการ state ที่คาดเดาได้และช่วยให้คุณอัปเดต state ตาม actions ซึ่งลดความจำเป็นในการส่งฟังก์ชัน setter หลายตัวผ่าน Context

ตัวอย่าง:


import React, { createContext, useReducer, useContext } from 'react';

const UserContext = createContext(null);

const initialState = {
  user: {
    name: 'John Doe',
    email: 'john.doe@example.com',
    location: 'New York, USA'
  },
  theme: 'light',
  language: 'en'
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_USER':
      return { ...state, user: { ...state.user, ...action.payload } };
    case 'TOGGLE_THEME':
      return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
    case 'SET_LANGUAGE':
      return { ...state, language: action.payload };
    default:
      return state;
  }
};

function UserProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    
      {children}
    
  );
}

function useUserState() {
  const { state } = useContext(UserContext);
  return state.user;
}

function useUserDispatch() {
    const { dispatch } = useContext(UserContext);
    return dispatch;
}


export { UserContext, UserProvider, useUserState, useUserDispatch };

ตอนนี้ คอมโพเนนต์สามารถเข้าถึง state และ dispatch actions โดยใช้ custom hooks ตัวอย่างเช่น:


import React from 'react';
import { useUserState, useUserDispatch } from './UserContext';

function UserProfile() {
  const user = useUserState();
  const dispatch = useUserDispatch();

  const handleUpdateName = (e) => {
    dispatch({ type: 'UPDATE_USER', payload: { name: e.target.value } });
  };

  return (
    

Name: {user.name}

); }

รูปแบบนี้ส่งเสริมแนวทางการจัดการ state ที่มีโครงสร้างมากขึ้นและสามารถทำให้ตรรกะของ Context ที่ซับซ้อนง่ายขึ้น

ข้อดี:

ข้อเสีย:

รูปแบบที่ 6: Optimistic Updates

Optimistic updates คือการอัปเดต UI ทันทีเสมือนว่าการกระทำสำเร็จแล้ว แม้กระทั่งก่อนที่เซิร์ฟเวอร์จะยืนยันก็ตาม ซึ่งสามารถปรับปรุงประสบการณ์ผู้ใช้ได้อย่างมาก โดยเฉพาะในสถานการณ์ที่มีความหน่วงสูง อย่างไรก็ตาม มันต้องการการจัดการข้อผิดพลาดที่อาจเกิดขึ้นอย่างรอบคอบ

ตัวอย่าง:

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


import React, { useContext, useState } from 'react';
import { UserContext } from './UserContext';

function LikeButton({ postId }) {
  const { dispatch } = useContext(UserContext);
  const [isLiking, setIsLiking] = useState(false);

  const handleLike = async () => {
    setIsLiking(true);
    // อัปเดตจำนวนไลค์แบบ optimistic
    dispatch({ type: 'INCREMENT_LIKES', payload: { postId } });

    try {
      // จำลองการเรียก API
      await new Promise(resolve => setTimeout(resolve, 500));

      // หากการเรียก API สำเร็จ ไม่ต้องทำอะไร (UI อัปเดตแล้ว)
    } catch (error) {
      // หากการเรียก API ล้มเหลว ย้อนกลับการอัปเดตแบบ optimistic
      dispatch({ type: 'DECREMENT_LIKES', payload: { postId } });
      alert('Failed to like post. Please try again.');
    } finally {
      setIsLiking(false);
    }
  };

  return (
    
  );
}

ในตัวอย่างนี้ action `INCREMENT_LIKES` จะถูก dispatch ทันที และจะถูกย้อนกลับหากการเรียก API ล้มเหลว ซึ่งจะมอบประสบการณ์ผู้ใช้ที่ตอบสนองได้ดีขึ้น

ข้อดี:

ข้อเสีย:

การเลือกรูปแบบที่เหมาะสม

รูปแบบ Context Provider ที่ดีที่สุดขึ้นอยู่กับความต้องการเฉพาะของแอปพลิเคชันของคุณ นี่คือสรุปเพื่อช่วยให้คุณเลือก:

เคล็ดลับเพิ่มเติมในการเพิ่มประสิทธิภาพของ Context

สรุป

React Context API เป็นเครื่องมือที่ทรงพลัง แต่จำเป็นต้องใช้อย่างถูกต้องเพื่อหลีกเลี่ยงปัญหาด้านประสิทธิภาพ โดยการทำความเข้าใจและนำรูปแบบ Context Provider ที่กล่าวถึงในบทความนี้ไปใช้ คุณจะสามารถจัดการ state, เพิ่มประสิทธิภาพ และสร้างแอปพลิเคชัน React ที่มีประสิทธิภาพและตอบสนองได้ดียิ่งขึ้น อย่าลืมวิเคราะห์ความต้องการเฉพาะของคุณและเลือกรูปแบบที่เหมาะสมกับความต้องการของแอปพลิเคชันของคุณมากที่สุด

ด้วยการพิจารณามุมมองในระดับโลก นักพัฒนาควรตรวจสอบให้แน่ใจว่าโซลูชันการจัดการ state ทำงานได้อย่างราบรื่นในเขตเวลา รูปแบบสกุลเงิน และข้อกำหนดข้อมูลระดับภูมิภาคที่แตกต่างกัน ตัวอย่างเช่น ฟังก์ชันการจัดรูปแบบวันที่ภายใน Context ควรได้รับการปรับให้เข้ากับท้องถิ่นตามความต้องการหรือตำแหน่งของผู้ใช้ เพื่อให้แน่ใจว่าการแสดงวันที่ถูกต้องและสอดคล้องกันไม่ว่าผู้ใช้จะเข้าถึงแอปพลิเคชันจากที่ใดก็ตาม