ไทย

เจาะลึก useReducer hook ของ React เพื่อจัดการ state ที่ซับซ้อนของแอปพลิเคชันอย่างมีประสิทธิภาพ เพิ่มประสิทธิภาพและความสามารถในการบำรุงรักษาสำหรับโปรเจกต์ React ระดับโลก

รูปแบบ useReducer ของ React: การจัดการ State ที่ซับซ้อนอย่างเชี่ยวชาญ

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

ทำความเข้าใจความจำเป็นในการจัดการ State ที่ซับซ้อน

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

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

แนะนำ useReducer Hook

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

useReducer hook รับอาร์กิวเมนต์สองตัว:

hook นี้จะส่งคืนอาร์เรย์ที่มีสององค์ประกอบ:

ฟังก์ชัน Reducer

ฟังก์ชัน reducer คือหัวใจของรูปแบบ useReducer มันเป็น pure function ซึ่งหมายความว่าไม่ควรมี side effects ใดๆ (เช่น การเรียก API หรือการแก้ไขตัวแปรส่วนกลาง) และควรส่งคืนผลลัพธ์เดิมเสมอสำหรับอินพุตเดียวกัน ฟังก์ชัน reducer รับอาร์กิวเมนต์สองตัว:

ภายในฟังก์ชัน reducer คุณจะใช้คำสั่ง switch หรือ if/else if เพื่อจัดการกับ action type ต่างๆ และอัปเดต state ตามนั้น ซึ่งจะช่วยรวมศูนย์ตรรกะการอัปเดต state ของคุณและทำให้ง่ายต่อการทำความเข้าใจว่า state เปลี่ยนแปลงอย่างไรเพื่อตอบสนองต่อเหตุการณ์ต่างๆ

ฟังก์ชัน Dispatch

ฟังก์ชัน dispatch เป็นเมธอดที่คุณใช้เพื่อกระตุ้นการอัปเดต state เมื่อคุณเรียกใช้ dispatch(action), action จะถูกส่งไปยังฟังก์ชัน reducer ซึ่งจะทำการอัปเดต state ตาม type และ payload ของ action นั้น

ตัวอย่างการใช้งานจริง: การสร้างตัวนับ (Counter)

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


import React, { useReducer } from 'react';

// Define action types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';

// Define the reducer function
function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + 1 };
    case DECREMENT:
      return { count: state.count - 1 };
    case RESET:
      return { count: 0 };
    default:
      return state;
  }
}

function Counter() {
  // Initialize useReducer
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>
      <button onClick={() => dispatch({ type: DECREMENT })}>Decrement</button>
      <button onClick={() => dispatch({ type: RESET })}>Reset</button>
    </div>
  );
}

export default Counter;

ในตัวอย่างนี้:

การขยายตัวอย่างตัวนับ: การเพิ่ม Payload

ลองแก้ไขตัวนับเพื่อให้สามารถเพิ่มค่าตามจำนวนที่ระบุได้ สิ่งนี้จะแนะนำแนวคิดของ payload ใน action:


import React, { useReducer } from 'react';

const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';
const RESET = 'RESET';
const SET_VALUE = 'SET_VALUE';

function counterReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return { count: state.count + action.payload };
    case DECREMENT:
      return { count: state.count - action.payload };
    case RESET:
      return { count: 0 };
    case SET_VALUE:
      return { count: action.payload };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
  const [inputValue, setInputValue] = React.useState(1);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT, payload: parseInt(inputValue) || 1 })}>Increment by {inputValue}</button>
      <button onClick={() => dispatch({ type: DECREMENT, payload: parseInt(inputValue) || 1 })}>Decrement by {inputValue}</button>
      <button onClick={() => dispatch({ type: RESET })}>Reset</button>
       <input
         type="number"
         value={inputValue}
         onChange={(e) => setInputValue(e.target.value)}
       />
      </div>
  );
}

export default Counter;

ในตัวอย่างที่ขยายนี้:

ประโยชน์ของการใช้ useReducer

รูปแบบ useReducer มีข้อดีหลายประการเหนือกว่าการใช้ useState โดยตรงสำหรับการจัดการ state ที่ซับซ้อน:

เมื่อใดควรใช้ useReducer

แม้ว่า useReducer จะมีประโยชน์อย่างมาก แต่ก็ไม่ใช่ตัวเลือกที่เหมาะสมเสมอไป พิจารณาใช้ useReducer เมื่อ:

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

แนวคิดและเทคนิคขั้นสูง

การรวม useReducer กับ Context

สำหรับการจัดการ global state หรือการแชร์ state ระหว่างคอมโพเนนต์หลายๆ ตัว คุณสามารถรวม useReducer เข้ากับ Context API ของ React ได้ วิธีการนี้มักเป็นที่นิยมมากกว่า Redux สำหรับโปรเจกต์ขนาดเล็กถึงขนาดกลางที่คุณไม่ต้องการเพิ่ม dependency พิเศษ


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

// Define action types and reducer (as before)
const INCREMENT = 'INCREMENT';
// ... (other action types and the counterReducer function)

const CounterContext = createContext();

function CounterProvider({ children }) {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <CounterContext.Provider value={{ state, dispatch }}>
      {children}
    </CounterContext.Provider>
  );
}

function useCounter() {
  return useContext(CounterContext);
}

function Counter() {
  const { state, dispatch } = useCounter();

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: INCREMENT })}>Increment</button>
    </div>
  );
}

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  );
}

export default App;

ในตัวอย่างนี้:

การทดสอบ useReducer

การทดสอบ reducer นั้นตรงไปตรงมาเพราะเป็น pure function คุณสามารถทดสอบฟังก์ชัน reducer แยกต่างหากได้อย่างง่ายดายโดยใช้เฟรมเวิร์กการทดสอบ unit test เช่น Jest หรือ Mocha นี่คือตัวอย่างการใช้ Jest:


import { counterReducer } from './counterReducer'; // Assuming counterReducer is in a separate file

const INCREMENT = 'INCREMENT';

describe('counterReducer', () => {
  it('should increment the count', () => {
    const state = { count: 0 };
    const action = { type: INCREMENT };
    const newState = counterReducer(state, action);
    expect(newState.count).toBe(1);
  });

   it('should return the same state for unknown action types', () => {
        const state = { count: 10 };
        const action = { type: 'UNKNOWN_ACTION' };
        const newState = counterReducer(state, action);
        expect(newState).toBe(state); // Assert that the state hasn't changed
    });
});

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

การเพิ่มประสิทธิภาพด้วย Memoization

เมื่อทำงานกับ state ที่ซับซ้อนและการอัปเดตบ่อยครั้ง ลองพิจารณาใช้ useMemo เพื่อเพิ่มประสิทธิภาพของคอมโพเนนต์ของคุณ โดยเฉพาะอย่างยิ่งหากคุณมีค่าที่คำนวณมาจาก state ตัวอย่างเช่น:


import React, { useReducer, useMemo } from 'react';

function reducer(state, action) {
  // ... (reducer logic) 
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  // Calculate a derived value, memoizing it with useMemo
  const derivedValue = useMemo(() => {
    // Expensive calculation based on state
    return state.value1 + state.value2;
  }, [state.value1, state.value2]); // Dependencies:  recalculate only when these values change

  return (
    <div>
      <p>Derived Value: {derivedValue}</p>
      <button onClick={() => dispatch({ type: 'UPDATE_VALUE1', payload: 10 })}>Update Value 1</button>
      <button onClick={() => dispatch({ type: 'UPDATE_VALUE2', payload: 20 })}>Update Value 2</button>
    </div>
  );
}

ในตัวอย่างนี้ derivedValue จะถูกคำนวณใหม่ก็ต่อเมื่อ state.value1 หรือ state.value2 เปลี่ยนแปลงเท่านั้น ซึ่งช่วยป้องกันการคำนวณที่ไม่จำเป็นในทุกๆ การ re-render วิธีการนี้เป็นแนวทางปฏิบัติทั่วไปเพื่อให้แน่ใจว่าประสิทธิภาพการเรนเดอร์ดีที่สุด

ตัวอย่างและกรณีการใช้งานในโลกจริง

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

1. ตัวกรองสินค้า E-commerce

ลองนึกภาพเว็บไซต์ E-commerce (เช่น แพลตฟอร์มยอดนิยมอย่าง Amazon หรือ AliExpress ที่มีให้บริการทั่วโลก) ที่มีแคตตาล็อกสินค้าขนาดใหญ่ ผู้ใช้จำเป็นต้องกรองสินค้าตามเกณฑ์ต่างๆ (ช่วงราคา, แบรนด์, ขนาด, สี, ประเทศผู้ผลิต ฯลฯ) useReducer เหมาะอย่างยิ่งสำหรับการจัดการ state ของตัวกรอง


import React, { useReducer } from 'react';

const initialState = {
  priceRange: { min: 0, max: 1000 },
  brand: [], // Array of selected brands
  color: [], // Array of selected colors
  //... other filter criteria
};

function filterReducer(state, action) {
  switch (action.type) {
    case 'UPDATE_PRICE_RANGE':
      return { ...state, priceRange: action.payload };
    case 'TOGGLE_BRAND':
      const brand = action.payload;
      return { ...state, brand: state.brand.includes(brand) ? state.brand.filter(b => b !== brand) : [...state.brand, brand] };
    case 'TOGGLE_COLOR':
      // Similar logic for color filtering
      return { ...state, color: state.color.includes(action.payload) ? state.color.filter(c => c !== action.payload) : [...state.color, action.payload] };
    // ... other filter actions
    default:
      return state;
  }
}

function ProductFilter() {
  const [state, dispatch] = useReducer(filterReducer, initialState);

  // UI components for selecting filter criteria and triggering dispatch actions
  // For example: Range input for price, checkboxes for brands, etc.

  return (
    <div>
      <!-- Filter UI elements -->
    </div>
  );
}

ตัวอย่างนี้แสดงวิธีการจัดการเกณฑ์การกรองหลายรายการในลักษณะที่ควบคุมได้ เมื่อผู้ใช้ปรับเปลี่ยนการตั้งค่าตัวกรองใดๆ (ราคา, แบรนด์ ฯลฯ) reducer จะอัปเดต state ของตัวกรองตามนั้น จากนั้นคอมโพเนนต์ที่รับผิดชอบในการแสดงสินค้าจะใช้ state ที่อัปเดตแล้วเพื่อกรองสินค้าที่แสดง รูปแบบนี้สนับสนุนการสร้างระบบการกรองที่ซับซ้อนซึ่งพบได้ทั่วไปในแพลตฟอร์ม E-commerce ทั่วโลก

2. ฟอร์มหลายขั้นตอน (เช่น ฟอร์มการจัดส่งระหว่างประเทศ)

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


import React, { useReducer } from 'react';

const initialState = {
  step: 1, // Current step in the form
  formData: {
    firstName: '',
    lastName: '',
    address: '',
    city: '',
    country: '',
    // ... other form fields
  },
  errors: {},
};

function formReducer(state, action) {
  switch (action.type) {
    case 'NEXT_STEP':
      return { ...state, step: state.step + 1 };
    case 'PREV_STEP':
      return { ...state, step: state.step - 1 };
    case 'UPDATE_FIELD':
      return { ...state, formData: { ...state.formData, [action.payload.field]: action.payload.value } };
    case 'SET_ERRORS':
      return { ...state, errors: action.payload };
    case 'SUBMIT_FORM':
      // Handle form submission logic here, e.g., API calls
      return state;
    default:
      return state;
  }
}

function MultiStepForm() {
  const [state, dispatch] = useReducer(formReducer, initialState);

  // Rendering logic for each step of the form
  // Based on the current step in the state
  const renderStep = () => {
    switch (state.step) {
      case 1:
        return <Step1 formData={state.formData} dispatch={dispatch} />;
      case 2:
        return <Step2 formData={state.formData} dispatch={dispatch} />;
      // ... other steps
      default:
        return <p>Invalid Step</p>;
    }
  };

  return (
    <div>
      {renderStep()}
      <!-- Navigation buttons (Next, Previous, Submit) based on the current step -->
    </div>
  );
}

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

3. แอปพลิเคชันแบบเรียลไทม์ (แชท, เครื่องมือทำงานร่วมกัน)

useReducer มีประโยชน์สำหรับแอปพลิเคชันแบบเรียลไทม์ เช่น เครื่องมือทำงานร่วมกันอย่าง Google Docs หรือแอปพลิเคชันส่งข้อความ มันจัดการกับเหตุการณ์ต่างๆ เช่น การรับข้อความ, การเข้าร่วม/ออกจากห้องของผู้ใช้, และสถานะการเชื่อมต่อ เพื่อให้แน่ใจว่า UI จะอัปเดตตามที่ต้องการ


import React, { useReducer, useEffect } from 'react';

const initialState = {
  messages: [],
  users: [],
  connectionStatus: 'connecting',
};

function chatReducer(state, action) {
  switch (action.type) {
    case 'RECEIVE_MESSAGE':
      return { ...state, messages: [...state.messages, action.payload] };
    case 'USER_JOINED':
      return { ...state, users: [...state.users, action.payload] };
    case 'USER_LEFT':
      return { ...state, users: state.users.filter(user => user.id !== action.payload.id) };
    case 'SET_CONNECTION_STATUS':
      return { ...state, connectionStatus: action.payload };
    default:
      return state;
  }
}

function ChatRoom() {
  const [state, dispatch] = useReducer(chatReducer, initialState);

  useEffect(() => {
    // Establish WebSocket connection (example):
    const socket = new WebSocket('wss://your-websocket-server.com');

    socket.onopen = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'connected' });
    socket.onmessage = (event) => dispatch({ type: 'RECEIVE_MESSAGE', payload: JSON.parse(event.data) });
    socket.onclose = () => dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'disconnected' });

    return () => socket.close(); // Cleanup on unmount
  }, []);

  // Render messages, user list, and connection status based on the state
  return (
    <div>
      <p>Connection Status: {state.connectionStatus}</p>
      <!-- UI for displaying messages, user list, and sending messages -->
    </div>
  );
}

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

แนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้ useReducer

เพื่อใช้ useReducer อย่างมีประสิทธิภาพและสร้างแอปพลิเคชันที่บำรุงรักษาได้ ลองพิจารณาแนวทางปฏิบัติที่ดีที่สุดเหล่านี้:

บทสรุป

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

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