ไทย

คู่มือฉบับสมบูรณ์เกี่ยวกับการจัดการ state ใน React สำหรับนักพัฒนาทั่วโลก สำรวจ useState, Context API, useReducer และไลบรารียอดนิยมอย่าง Redux, Zustand และ TanStack Query

เชี่ยวชาญการจัดการ State ใน React: คู่มือสำหรับนักพัฒนาทั่วโลก

ในโลกของการพัฒนา front-end การจัดการ state ถือเป็นหนึ่งในความท้าทายที่สำคัญที่สุด สำหรับนักพัฒนาที่ใช้ React ความท้าทายนี้ได้พัฒนาจากเรื่องง่ายๆ ในระดับคอมโพเนนต์ไปสู่การตัดสินใจเชิงสถาปัตยกรรมที่ซับซ้อน ซึ่งสามารถกำหนดความสามารถในการขยายขนาด (scalability) ประสิทธิภาพ และความง่ายในการบำรุงรักษา (maintainability) ของแอปพลิเคชันได้เลย ไม่ว่าคุณจะเป็นนักพัฒนาเดี่ยวในสิงคโปร์ เป็นส่วนหนึ่งของทีมที่กระจายตัวอยู่ทั่วยุโรป หรือเป็นผู้ก่อตั้งสตาร์ทอัพในบราซิล การทำความเข้าใจภาพรวมของการจัดการ state ใน React ถือเป็นสิ่งจำเป็นสำหรับการสร้างแอปพลิเคชันที่มีความเสถียรและเป็นมืออาชีพ

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

'State' ใน React คืออะไร และทำไมจึงสำคัญมาก?

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

React ถูกสร้างขึ้นบนหลักการที่ว่า UI เป็นฟังก์ชันของ state (UI = f(state)) เมื่อ state เปลี่ยนแปลง React จะทำการ re-render ส่วนที่จำเป็นของ UI ใหม่อย่างมีประสิทธิภาพเพื่อสะท้อนการเปลี่ยนแปลงนั้น ความท้าทายจะเกิดขึ้นเมื่อ state นี้จำเป็นต้องถูกแชร์และแก้ไขโดยคอมโพเนนต์หลายตัวที่ไม่ได้มีความสัมพันธ์กันโดยตรงใน component tree นี่คือจุดที่การจัดการ state กลายเป็นข้อกังวลสำคัญทางสถาปัตยกรรม

พื้นฐาน: Local State ด้วย useState

เส้นทางของนักพัฒนา React ทุกคนเริ่มต้นด้วย useState hook มันเป็นวิธีที่ง่ายที่สุดในการประกาศ state ที่เป็นแบบ local ภายในคอมโพเนนต์เดียว

ตัวอย่างเช่น การจัดการ state ของ counter แบบง่ายๆ:


import React, { useState } from 'react';

function Counter() {
  // 'count' คือตัวแปร state
  // 'setCount' คือฟังก์ชันสำหรับอัปเดตค่า
  const [count, setCount] = useState(0);

  return (
    

คุณคลิกไปแล้ว {count} ครั้ง

); }

useState เหมาะสมอย่างยิ่งสำหรับ state ที่ไม่จำเป็นต้องแชร์ เช่น ข้อมูลในฟอร์ม, การสลับเปิด/ปิด (toggles) หรือองค์ประกอบ UI ใดๆ ที่สภาวะของมันไม่ส่งผลกระทบต่อส่วนอื่นๆ ของแอปพลิเคชัน ปัญหาจะเริ่มขึ้นเมื่อคุณต้องการให้คอมโพเนนต์อื่นรับรู้ค่าของ `count`

แนวทางคลาสสิก: Lifting State Up และ Prop Drilling

วิธีการดั้งเดิมของ React ในการแชร์ state ระหว่างคอมโพเนนต์คือการ "ยกมันขึ้นไป" (lift it up) ไว้ที่ ancestor (คอมโพเนนต์บรรพบุรุษ) ร่วมที่ใกล้ที่สุด จากนั้น state จะถูกส่งลงไปยังคอมโพเนนต์ลูกผ่านทาง props นี่เป็นรูปแบบพื้นฐานและสำคัญของ React

อย่างไรก็ตาม เมื่อแอปพลิเคชันเติบโตขึ้น สิ่งนี้อาจนำไปสู่ปัญหาที่เรียกว่า "prop drilling" ซึ่งคือการที่คุณต้องส่ง props ผ่านคอมโพเนนต์ตัวกลางหลายชั้นที่ไม่ต้องการข้อมูลนั้นจริงๆ เพียงเพื่อส่งต่อไปยังคอมโพเนนต์ลูกที่อยู่ลึกเข้าไปซึ่งต้องการใช้ข้อมูลนั้น สิ่งนี้อาจทำให้โค้ดอ่านยาก, refactor ยาก และบำรุงรักษายากขึ้น

ลองจินตนาการถึงการตั้งค่า theme ของผู้ใช้ (เช่น 'dark' หรือ 'light') ที่ปุ่มซึ่งอยู่ลึกลงไปใน component tree ต้องการเข้าถึง คุณอาจต้องส่งมันไปแบบนี้: App -> Layout -> Page -> Header -> ThemeToggleButton มีเพียง App (ที่ state ถูกกำหนด) และ ThemeToggleButton (ที่ state ถูกใช้) เท่านั้นที่สนใจ prop นี้ แต่ Layout, Page, และ Header ถูกบังคับให้ทำหน้าที่เป็นตัวกลาง นี่คือปัญหาที่โซลูชันการจัดการ state ขั้นสูงมุ่งหวังที่จะแก้ไข

โซลูชันในตัวของ React: พลังของ Context และ Reducers

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

1. The Context API: การกระจาย State ไปทั่วทั้งแอป

Context API เป็นวิธีการส่งข้อมูลผ่าน component tree โดยไม่ต้องส่ง props ลงไปทีละระดับด้วยตนเอง ลองนึกภาพว่ามันเป็นที่เก็บข้อมูลแบบ global สำหรับส่วนใดส่วนหนึ่งของแอปพลิเคชันของคุณ

การใช้ Context มี 3 ขั้นตอนหลัก:

  1. สร้าง Context: ใช้ `React.createContext()` เพื่อสร้าง context object
  2. Provide Context: ใช้คอมโพเนนต์ `Context.Provider` เพื่อครอบส่วนหนึ่งของ component tree ของคุณและส่ง `value` ไปให้มัน คอมโพเนนต์ใดๆ ภายใน provider นี้จะสามารถเข้าถึง value นั้นได้
  3. Consume Context: ใช้ `useContext` hook ภายในคอมโพเนนต์เพื่อ subscribe context และรับค่าปัจจุบันของมัน

ตัวอย่าง: ตัวสลับ theme อย่างง่ายโดยใช้ Context


// 1. สร้าง Context (เช่น ในไฟล์ theme-context.js)
import { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

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

  // value object นี้จะพร้อมใช้งานสำหรับคอมโพเนนต์ consumer ทั้งหมด
  const value = { theme, toggleTheme };

  return (
    
      {children}
    
  );
}

// 2. Provide Context (เช่น ในไฟล์ App.js หลักของคุณ)
import { ThemeProvider } from './theme-context';
import MyPage from './MyPage';

function App() {
  return (
    
      
    
  );
}

// 3. Consume Context (เช่น ในคอมโพเนนต์ที่ซ้อนกันอยู่ลึกๆ)
import { useContext } from 'react';
import { ThemeContext } from './theme-context';

function ThemeToggleButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    
  );
}

ข้อดีของ Context API:

ข้อเสียและข้อควรพิจารณาด้านประสิทธิภาพ:

2. The `useReducer` Hook: สำหรับการเปลี่ยนแปลง State ที่คาดเดาได้

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

`useReducer` ได้รับแรงบันดาลใจจาก Redux โดยเกี่ยวข้องกับฟังก์ชัน `reducer` และฟังก์ชัน `dispatch`:

ตัวอย่าง: counter ที่มี action เพิ่ม, ลด และรีเซ็ต


import React, { useReducer } from 'react';

// 1. กำหนด state เริ่มต้น
const initialState = { count: 0 };

// 2. สร้างฟังก์ชัน reducer
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      throw new Error('Unexpected action type');
  }
}

function ReducerCounter() {
  // 3. เริ่มต้นการใช้งาน useReducer
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      

Count: {state.count}

{/* 4. Dispatch actions เมื่อผู้ใช้โต้ตอบ */} ); }

การใช้ `useReducer` จะรวมศูนย์ตรรกะการอัปเดต state ของคุณไว้ในที่เดียว (ฟังก์ชัน reducer) ทำให้คาดเดาได้ง่ายขึ้น ทดสอบง่ายขึ้น และบำรุงรักษาได้ง่ายขึ้น โดยเฉพาะอย่างยิ่งเมื่อตรรกะมีความซับซ้อนเพิ่มขึ้น

คู่หูทรงพลัง: `useContext` + `useReducer`

พลังที่แท้จริงของ hooks ที่มีมาในตัวของ React จะปรากฏเมื่อคุณรวม `useContext` และ `useReducer` เข้าด้วยกัน รูปแบบนี้ช่วยให้คุณสร้างโซลูชันการจัดการ state ที่แข็งแกร่งคล้าย Redux ได้โดยไม่ต้องมี dependency ภายนอกใดๆ

รูปแบบนี้ยอดเยี่ยมมากเพราะฟังก์ชัน `dispatch` เองมี identity ที่เสถียรและจะไม่เปลี่ยนแปลงระหว่างการ re-render ซึ่งหมายความว่าคอมโพเนนต์ที่ต้องการเพียงแค่ `dispatch` actions จะไม่ re-render โดยไม่จำเป็นเมื่อค่า state เปลี่ยนแปลง ซึ่งเป็นการเพิ่มประสิทธิภาพในตัว

ตัวอย่าง: การจัดการตะกร้าสินค้าอย่างง่าย


// 1. ตั้งค่าใน cart-context.js
import { createContext, useReducer, useContext } from 'react';

const CartStateContext = createContext();
const CartDispatchContext = createContext();

const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      // ตรรกะในการเพิ่มสินค้า
      return [...state, action.payload];
    case 'REMOVE_ITEM':
      // ตรรกะในการลบสินค้าตาม id
      return state.filter(item => item.id !== action.payload.id);
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
};

export const CartProvider = ({ children }) => {
  const [state, dispatch] = useReducer(cartReducer, []);

  return (
    
      
        {children}
      
    
  );
};

// Custom hooks เพื่อให้เรียกใช้ง่าย
export const useCart = () => useContext(CartStateContext);
export const useCartDispatch = () => useContext(CartDispatchContext);

// 2. การใช้งานในคอมโพเนนต์
// ProductComponent.js - ต้องการแค่ dispatch action
function ProductComponent({ product }) {
  const dispatch = useCartDispatch();
  
  const handleAddToCart = () => {
    dispatch({ type: 'ADD_ITEM', payload: product });
  };

  return ;
}

// CartDisplayComponent.js - ต้องการแค่อ่าน state
function CartDisplayComponent() {
  const cartItems = useCart();

  return 
จำนวนสินค้าในตะกร้า: {cartItems.length}
; }

ด้วยการแยก state และ dispatch ออกเป็นสอง context ที่แยกจากกัน เราจะได้รับประโยชน์ด้านประสิทธิภาพ: คอมโพเนนต์อย่าง `ProductComponent` ที่ทำหน้าที่เพียง dispatch actions จะไม่ re-render เมื่อ state ของตะกร้าเปลี่ยนแปลง

เมื่อใดที่ควรใช้ไลบรารีภายนอก

รูปแบบ `useContext` + `useReducer` นั้นทรงพลัง แต่ก็ไม่ใช่กระสุนเงิน (silver bullet) เมื่อแอปพลิเคชันขยายขนาด คุณอาจพบกับความต้องการที่ไลบรารีภายนอกที่ออกแบบมาโดยเฉพาะสามารถตอบสนองได้ดีกว่า คุณควรพิจารณาไลบรารีภายนอกเมื่อ:

ทัวร์รอบโลกของไลบรารีจัดการ State ยอดนิยม

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

1. Redux (& Redux Toolkit): มาตรฐานที่ได้รับการยอมรับ

Redux เป็นไลบรารีการจัดการ state ที่ครองตลาดมานานหลายปี มันบังคับใช้การไหลของข้อมูลแบบทิศทางเดียวที่เข้มงวด ทำให้การเปลี่ยนแปลง state สามารถคาดเดาและติดตามได้ ในขณะที่ Redux ในยุคแรกมีชื่อเสียงในเรื่อง boilerplate แต่แนวทางสมัยใหม่ที่ใช้ Redux Toolkit (RTK) ได้ทำให้กระบวนการต่างๆ ง่ายขึ้นอย่างมาก

2. Zustand: ตัวเลือกที่เรียบง่ายและไม่มีข้อกำหนดตายตัว

Zustand ซึ่งหมายถึง "state" ในภาษาเยอรมัน นำเสนอแนวทางที่เรียบง่ายและยืดหยุ่น มักถูกมองว่าเป็นทางเลือกที่ง่ายกว่า Redux โดยให้ประโยชน์ของ store แบบรวมศูนย์โดยไม่มี boilerplate


// store.js
import { create } from 'zustand';

const useBearStore = create((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}));

// MyComponent.js
function BearCounter() {
  const bears = useBearStore((state) => state.bears);
  return 

{bears} around here ...

; } function Controls() { const increasePopulation = useBearStore((state) => state.increasePopulation); return ; }

3. Jotai & Recoil: แนวทางแบบ Atomic

Jotai และ Recoil (จาก Facebook) ทำให้แนวคิดของการจัดการ state แบบ "atomic" เป็นที่นิยม แทนที่จะมี state object ขนาดใหญ่เพียงก้อนเดียว คุณจะแบ่ง state ของคุณออกเป็นส่วนเล็กๆ ที่เป็นอิสระต่อกันเรียกว่า "atoms"

4. TanStack Query (เดิมชื่อ React Query): ราชาแห่ง Server State

บางทีการเปลี่ยนแปลงกระบวนทัศน์ที่สำคัญที่สุดในช่วงไม่กี่ปีที่ผ่านมาคือการตระหนักว่าสิ่งที่เราเรียกว่า "state" ส่วนใหญ่นั้น จริงๆ แล้วคือ server state — ข้อมูลที่อยู่บนเซิร์ฟเวอร์ ซึ่งถูกดึงมา, แคช และซิงโครไนซ์ในแอปพลิเคชันฝั่ง client ของเรา TanStack Query ไม่ใช่ตัวจัดการ state ทั่วไป แต่เป็นเครื่องมือพิเศษสำหรับจัดการ server state และมันทำหน้าที่นี้ได้ดีเป็นพิเศษ

การเลือกสิ่งที่ใช่: กรอบการตัดสินใจ

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

  1. state นั้นเป็น global จริงๆ หรือสามารถเป็น local ได้?
    เริ่มต้นด้วย useState เสมอ อย่าเพิ่งนำ global state เข้ามาใช้หากไม่จำเป็นจริงๆ
  2. ข้อมูลที่คุณกำลังจัดการเป็น server state หรือไม่?
    หากเป็นข้อมูลจาก API ให้ใช้ TanStack Query สิ่งนี้จะจัดการการแคช, การดึงข้อมูล และการซิงโครไนซ์ให้คุณ และมีแนวโน้มที่จะจัดการ "state" ของแอปคุณได้ถึง 80%
  3. สำหรับ UI state ที่เหลือ คุณแค่ต้องการหลีกเลี่ยง prop drilling ใช่หรือไม่?
    หาก state อัปเดตไม่บ่อย (เช่น theme, ข้อมูลผู้ใช้, ภาษา) Context API ที่มีมาในตัวเป็นโซลูชันที่สมบูรณ์แบบและไม่ต้องพึ่งพา dependency ภายนอก
  4. ตรรกะ UI state ของคุณซับซ้อนและมีการเปลี่ยนแปลงที่คาดเดาได้หรือไม่?
    รวม useReducer กับ Context เข้าด้วยกัน สิ่งนี้ให้วิธีการจัดการตรรกะของ state ที่มีประสิทธิภาพและเป็นระเบียบโดยไม่ต้องใช้ไลบรารีภายนอก
  5. คุณกำลังประสบปัญหาด้านประสิทธิภาพกับ Context หรือ state ของคุณประกอบด้วยส่วนย่อยที่เป็นอิสระจำนวนมากหรือไม่?
    พิจารณาตัวจัดการ state แบบ atomic อย่าง Jotai มันมี API ที่เรียบง่ายพร้อมประสิทธิภาพที่ยอดเยี่ยมโดยการป้องกันการ re-render ที่ไม่จำเป็น
  6. คุณกำลังสร้างแอปพลิเคชันระดับองค์กรขนาดใหญ่ที่ต้องการสถาปัตยกรรมที่เข้มงวด, คาดเดาได้, middleware และเครื่องมือดีบักที่ทรงพลังหรือไม่?
    นี่คือกรณีการใช้งานหลักสำหรับ Redux Toolkit โครงสร้างและระบบนิเวศของมันถูกออกแบบมาเพื่อความซับซ้อนและการบำรุงรักษาระยะยาวในทีมขนาดใหญ่

ตารางสรุปเปรียบเทียบ

โซลูชัน เหมาะสำหรับ ข้อได้เปรียบหลัก ความยากในการเรียนรู้
useState Local component state เรียบง่าย, มีมาในตัว ต่ำมาก
Context API Global state ที่อัปเดตไม่บ่อย (theme, auth) แก้ปัญหา prop drilling, มีมาในตัว ต่ำ
useReducer + Context UI state ที่ซับซ้อนโดยไม่ใช้ไลบรารีภายนอก ตรรกะที่เป็นระเบียบ, มีมาในตัว ปานกลาง
TanStack Query Server state (การแคช/ซิงค์ข้อมูล API) ขจัดตรรกะของ state จำนวนมหาศาล ปานกลาง
Zustand / Jotai Global state ที่เรียบง่าย, การเพิ่มประสิทธิภาพ Boilerplate น้อย, ประสิทธิภาพเยี่ยม ต่ำ
Redux Toolkit แอปขนาดใหญ่ที่มี state ซับซ้อนและใช้ร่วมกัน คาดเดาได้, dev tools ทรงพลัง, ระบบนิเวศ สูง

สรุป: มุมมองเชิงปฏิบัติและระดับโลก

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

สำหรับโปรเจกต์ส่วนใหญ่ทั่วโลก stack ที่ทรงพลังและมีประสิทธิภาพเริ่มต้นด้วย:

  1. TanStack Query สำหรับ server state ทั้งหมด
  2. useState สำหรับ UI state ง่ายๆ ที่ไม่แชร์ทั้งหมด
  3. useContext สำหรับ global UI state ที่เรียบง่ายและอัปเดตไม่บ่อย

คุณควรหันไปใช้ไลบรารี global state โดยเฉพาะอย่าง Jotai, Zustand หรือ Redux Toolkit ก็ต่อเมื่อเครื่องมือเหล่านี้ไม่เพียงพอเท่านั้น ด้วยการแยกแยะระหว่าง server state และ client state อย่างชัดเจน และโดยการเริ่มต้นด้วยโซลูชันที่ง่ายที่สุดก่อน คุณจะสามารถสร้างแอปพลิเคชันที่มีประสิทธิภาพ, ขยายขนาดได้ และบำรุงรักษาได้อย่างมีความสุข ไม่ว่าทีมของคุณจะมีขนาดเท่าใดหรือผู้ใช้ของคุณจะอยู่ที่ไหนก็ตาม