ไทย

สำรวจรูปแบบขั้นสูงสำหรับ React Context API รวมถึง compound components, dynamic contexts และเทคนิคการเพิ่มประสิทธิภาพสำหรับจัดการ state ที่ซับซ้อน

รูปแบบขั้นสูงของ React Context API สำหรับการจัดการ State

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

ทำความเข้าใจข้อจำกัดของ Context API พื้นฐาน

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

รูปแบบที่ 1: Compound Components ร่วมกับ Context

รูปแบบ Compound Component ช่วยเสริมการทำงานของ Context API โดยการสร้างชุดของ components ที่เกี่ยวข้องกันซึ่งแชร์ state และ logic ผ่าน context โดยปริยาย รูปแบบนี้ส่งเสริมการนำกลับมาใช้ใหม่และทำให้ API สำหรับผู้ใช้งานง่ายขึ้น ซึ่งช่วยให้สามารถห่อหุ้ม logic ที่ซับซ้อนไว้ในการใช้งานที่เรียบง่าย

ตัวอย่าง: Tab Component

เรามาดูตัวอย่างนี้ด้วย Tab component แทนที่จะส่ง props ลงไปหลายๆ ชั้น Tab components จะสื่อสารกันโดยปริยายผ่าน context ที่ใช้ร่วมกัน

// TabContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface TabContextType {
  activeTab: string;
  setActiveTab: (tab: string) => void;
}

const TabContext = createContext(undefined);

interface TabProviderProps {
  children: ReactNode;
  defaultTab: string;
}

export const TabProvider: React.FC = ({ children, defaultTab }) => {
  const [activeTab, setActiveTab] = useState(defaultTab);

  const value: TabContextType = {
    activeTab,
    setActiveTab,
  };

  return {children};
};

export const useTabContext = () => {
  const context = useContext(TabContext);
  if (!context) {
    throw new Error('useTabContext must be used within a TabProvider');
  }
  return context;
};

// TabList.js
import React, { ReactNode } from 'react';

interface TabListProps {
  children: ReactNode;
}

export const TabList: React.FC = ({ children }) => {
  return 
{children}
; }; // Tab.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabProps { label: string; children: ReactNode; } export const Tab: React.FC = ({ label, children }) => { const { activeTab, setActiveTab } = useTabContext(); const isActive = activeTab === label; const handleClick = () => { setActiveTab(label); }; return ( ); }; // TabPanel.js import React, { ReactNode } from 'react'; import { useTabContext } from './TabContext'; interface TabPanelProps { label: string; children: ReactNode; } export const TabPanel: React.FC = ({ label, children }) => { const { activeTab } = useTabContext(); const isActive = activeTab === label; return ( ); };
// Usage
import { TabProvider, TabList, Tab, TabPanel } from './components/Tabs';

function App() {
  return (
    
      
        Tab 1
        Tab 2
        Tab 3
      
      Content for Tab 1
      Content for Tab 2
      Content for Tab 3
    
  );
}

export default App;

ประโยชน์:

รูปแบบที่ 2: Dynamic Contexts

ในบางสถานการณ์ คุณอาจต้องการค่า context ที่แตกต่างกันขึ้นอยู่กับตำแหน่งของ component ใน component tree หรือปัจจัยไดนามิกอื่นๆ Dynamic contexts ช่วยให้คุณสามารถสร้างและให้ค่า context ที่แตกต่างกันไปตามเงื่อนไขเฉพาะได้

ตัวอย่าง: การทำ Theming ด้วย Dynamic Contexts

ลองพิจารณาระบบการทำ a theming ที่คุณต้องการให้มี theme ที่แตกต่างกันตามความชอบของผู้ใช้หรือส่วนของแอปพลิเคชันที่พวกเขากำลังใช้งานอยู่ เราสามารถสร้างตัวอย่างง่ายๆ ด้วย light และ dark theme

// ThemeContext.js
import React, { createContext, useContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = () => {
    setIsDarkTheme(!isDarkTheme);
  };

  const value: ThemeContextType = {
    theme,
    toggleTheme,
  };

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};
// Usage
import { useTheme, ThemeProvider } from './ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useTheme();

  return (
    

This is a themed component.

); } function App() { return ( ); } export default App;

ในตัวอย่างนี้ ThemeProvider จะกำหนด theme แบบไดนามิกตาม state ของ isDarkTheme Components ที่ใช้ hook useTheme จะ re-render ใหม่โดยอัตโนมัติเมื่อ theme เปลี่ยนแปลง

รูปแบบที่ 3: Context ร่วมกับ useReducer สำหรับ State ที่ซับซ้อน

สำหรับการจัดการ logic ของ state ที่ซับซ้อน การรวม Context API กับ useReducer เป็นแนวทางที่ยอดเยี่ยม useReducer ให้วิธีที่เป็นโครงสร้างในการอัปเดต state ตาม actions และ Context API ช่วยให้คุณสามารถแชร์ state นี้และฟังก์ชัน dispatch ทั่วทั้งแอปพลิเคชันของคุณได้

ตัวอย่าง: Todo List แบบง่าย

// TodoContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

interface TodoContextType {
  state: TodoState;
  dispatch: React.Dispatch;
}

const initialState: TodoState = {
  todos: [],
};

const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};

const TodoContext = createContext(undefined);

interface TodoProviderProps {
  children: ReactNode;
}

export const TodoProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(todoReducer, initialState);

  const value: TodoContextType = {
    state,
    dispatch,
  };

  return {children};
};

export const useTodo = () => {
  const context = useContext(TodoContext);
  if (!context) {
    throw new Error('useTodo must be used within a TodoProvider');
  }
  return context;
};
// Usage
import { useTodo, TodoProvider } from './TodoContext';

function TodoList() {
  const { state, dispatch } = useTodo();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function AddTodo() { const { dispatch } = useTodo(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

รูปแบบนี้จะรวมศูนย์ logic การจัดการ state ไว้ใน reducer ทำให้ง่ายต่อการทำความเข้าใจและทดสอบ Components สามารถ dispatch actions เพื่ออัปเดต state ได้โดยไม่ต้องจัดการ state โดยตรง

รูปแบบที่ 4: การอัปเดต Context อย่างมีประสิทธิภาพด้วย `useMemo` และ `useCallback`

ดังที่ได้กล่าวไว้ก่อนหน้านี้ ข้อควรพิจารณาที่สำคัญด้านประสิทธิภาพของ Context API คือการ re-render ที่ไม่จำเป็น การใช้ useMemo และ useCallback สามารถป้องกันการ re-render เหล่านี้ได้โดยทำให้แน่ใจว่ามีการอัปเดตเฉพาะส่วนที่จำเป็นของค่า context และ reference ของฟังก์ชันยังคงเสถียร

ตัวอย่าง: การเพิ่มประสิทธิภาพ Theme Context

// OptimizedThemeContext.js
import React, { createContext, useContext, useState, useMemo, useCallback, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

const defaultTheme: Theme = {
    background: 'white',
    color: 'black'
};

const darkTheme: Theme = {
    background: 'black',
    color: 'white'
};

const ThemeContext = createContext({
    theme: defaultTheme,
    toggleTheme: () => {}
});

interface ThemeProviderProps {
  children: ReactNode;
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  const theme = isDarkTheme ? darkTheme : defaultTheme;

  const toggleTheme = useCallback(() => {
    setIsDarkTheme(!isDarkTheme);
  }, [isDarkTheme]);

  const value: ThemeContextType = useMemo(() => ({
    theme,
    toggleTheme,
  }), [theme, toggleTheme]);

  return {children};
};

export const useTheme = () => {
  return useContext(ThemeContext);
};

คำอธิบาย:

หากไม่มี useCallback ฟังก์ชัน toggleTheme จะถูกสร้างขึ้นใหม่ทุกครั้งที่มีการ render ThemeProvider ซึ่งจะทำให้ value เปลี่ยนแปลงและกระตุ้นให้เกิดการ re-render ใน components ที่ใช้งาน แม้ว่า theme เองจะไม่ได้เปลี่ยนแปลงก็ตาม useMemo ช่วยให้มั่นใจว่า value ใหม่จะถูกสร้างขึ้นก็ต่อเมื่อ dependencies ของมัน (theme หรือ toggleTheme) เปลี่ยนแปลงเท่านั้น

รูปแบบที่ 5: Context Selectors

Context selectors ช่วยให้ components สามารถติดตาม (subscribe) เฉพาะบางส่วนของค่า context ได้ ซึ่งจะช่วยป้องกันการ re-render ที่ไม่จำเป็นเมื่อส่วนอื่นๆ ของ context เปลี่ยนแปลง สามารถใช้ไลบรารีอย่าง `use-context-selector` หรือการสร้างขึ้นเองเพื่อให้ได้ผลลัพธ์นี้

ตัวอย่างการใช้ Custom Context Selector

// useCustomContextSelector.js
import { useContext, useState, useRef, useEffect } from 'react';

function useCustomContextSelector(
  context: React.Context,
  selector: (value: T) => S
): S {
  const value = useContext(context);
  const [selected, setSelected] = useState(() => selector(value));
  const latestSelector = useRef(selector);
  latestSelector.current = selector;

  useEffect(() => {
    let didUnmount = false;
    let lastSelected = selected;

    const subscription = () => {
      if (didUnmount) {
        return;
      }
      const nextSelected = latestSelector.current(value);
      if (!Object.is(lastSelected, nextSelected)) {
        lastSelected = nextSelected;
        setSelected(nextSelected);
      }
    };

    // You would typically subscribe to context changes here. Since this is a simplified
    // example, we'll just call subscription immediately to initialize.
    subscription();

    return () => {
      didUnmount = true;
      // Unsubscribe from context changes here, if applicable.
    };
  }, [value]); // Re-run effect whenever the context value changes

  return selected;
}

export default useCustomContextSelector;
// ThemeContext.js (Simplified for brevity)
import React, { createContext, useState, ReactNode } from 'react';

interface Theme {
  background: string;
  color: string;
}

interface ThemeContextType {
  theme: Theme;
  setTheme: (newTheme: Theme) => void; 
}

const ThemeContext = createContext(undefined);

interface ThemeProviderProps {
  children: ReactNode;
  initialTheme: Theme;
}

export const ThemeProvider: React.FC = ({ children, initialTheme }) => {
  const [theme, setTheme] = useState(initialTheme);

  const value: ThemeContextType = {
    theme,
    setTheme
  };

  return {children};
};

export const useThemeContext = () => {
    const context = React.useContext(ThemeContext);
    if (!context) {
        throw new Error("useThemeContext must be used within a ThemeProvider");
    }
    return context;
};

export default ThemeContext;
// Usage
import useCustomContextSelector from './useCustomContextSelector';
import ThemeContext, { ThemeProvider, useThemeContext } from './ThemeContext';

function BackgroundComponent() {
  const background = useCustomContextSelector(ThemeContext, (context) => context.theme.background);
  return 
Background
; } function ColorComponent() { const color = useCustomContextSelector(ThemeContext, (context) => context.theme.color); return
Color
; } function App() { const { theme, setTheme } = useThemeContext(); const toggleTheme = () => { setTheme({ background: theme.background === 'white' ? 'black' : 'white', color: theme.color === 'black' ? 'white' : 'black' }); }; return ( ); } export default App;

ในตัวอย่างนี้ BackgroundComponent จะ re-render ใหม่ก็ต่อเมื่อ property background ของ theme เปลี่ยนแปลงเท่านั้น และ ColorComponent จะ re-render ใหม่ก็ต่อเมื่อ property color เปลี่ยนแปลงเท่านั้น ซึ่งจะหลีกเลี่ยงการ re-render ที่ไม่จำเป็นเมื่อค่า context ทั้งหมดเปลี่ยนแปลง

รูปแบบที่ 6: การแยก Actions ออกจาก State

สำหรับแอปพลิเคชันขนาดใหญ่ ควรพิจารณาแยกค่า context ออกเป็นสอง context ที่แตกต่างกัน: context หนึ่งสำหรับ state และอีก context หนึ่งสำหรับ actions (ฟังก์ชัน dispatch) ซึ่งจะช่วยปรับปรุงการจัดระเบียบโค้ดและความสามารถในการทดสอบ

ตัวอย่าง: Todo List ที่มีการแยก State และ Action Contexts

// TodoStateContext.js
import React, { createContext, useContext, useReducer, ReactNode } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

interface TodoState {
  todos: Todo[];
}

const initialState: TodoState = {
  todos: [],
};

const TodoStateContext = createContext(initialState);

interface TodoStateProviderProps {
  children: ReactNode;
}

export const TodoStateProvider: React.FC = ({ children }) => {
  const [state] = useReducer(todoReducer, initialState);

  return {children};
};

export const useTodoState = () => {
  return useContext(TodoStateContext);
};

// TodoActionContext.js
import React, { createContext, useContext, Dispatch, ReactNode } from 'react';

type TodoAction = 
  | { type: 'ADD_TODO'; text: string } 
  | { type: 'TOGGLE_TODO'; id: number } 
  | { type: 'DELETE_TODO'; id: number };

const TodoActionContext = createContext | undefined>(undefined);

interface TodoActionProviderProps {
    children: ReactNode;
}

export const TodoActionProvider: React.FC = ({children}) => {
    const [, dispatch] = useReducer(todoReducer, initialState);

    return {children};
};


export const useTodoDispatch = () => {
  const dispatch = useContext(TodoActionContext);
  if (!dispatch) {
    throw new Error('useTodoDispatch must be used within a TodoActionProvider');
  }
  return dispatch;
};

// todoReducer.js
export const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.text, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.id),
      };
    default:
      return state;
  }
};
// Usage
import { useTodoState, TodoStateProvider } from './TodoStateContext';
import { useTodoDispatch, TodoActionProvider } from './TodoActionContext';

function TodoList() {
  const state = useTodoState();

  return (
    
    {state.todos.map((todo) => (
  • {todo.text}
  • ))}
); } function TodoActions({ todo }) { const dispatch = useTodoDispatch(); return ( <> ); } function AddTodo() { const dispatch = useTodoDispatch(); const [text, setText] = React.useState(''); const handleSubmit = (e) => { e.preventDefault(); dispatch({ type: 'ADD_TODO', text }); setText(''); }; return (
setText(e.target.value)} />
); } function App() { return ( ); } export default App;

การแยกส่วนนี้ช่วยให้ components สามารถติดตามเฉพาะ context ที่ต้องการได้ ซึ่งช่วยลดการ re-render ที่ไม่จำเป็น และยังทำให้การทำ unit test สำหรับ reducer และแต่ละ component แยกจากกันง่ายขึ้นด้วย นอกจากนี้ ลำดับการครอบ Provider ก็มีความสำคัญ โดย ActionProvider จะต้องครอบ StateProvider

แนวทางปฏิบัติที่ดีที่สุดและข้อควรพิจารณา

สรุป

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

เมื่อ React พัฒนาขึ้น แนวทางปฏิบัติที่ดีที่สุดเกี่ยวกับ Context API ก็จะพัฒนาไปด้วย การติดตามข่าวสารเกี่ยวกับเทคนิคและไลบรารีใหม่ๆ จะช่วยให้คุณพร้อมรับมือกับความท้าทายในการจัดการ state ของการพัฒนาเว็บสมัยใหม่ ลองพิจารณาสำรวจรูปแบบใหม่ๆ ที่เกิดขึ้น เช่น การใช้ context กับ signals เพื่อให้ได้ reactivity ที่ละเอียดมากยิ่งขึ้น