React Hooks ขั้นสูง: รูปแบบและการเพิ่มประสิทธิภาพ

เชี่ยวชาญ React Hooks ขั้นสูงด้วยรูปแบบที่พิสูจน์แล้ว Custom hooks, useEffect ที่ปรับแต่ง, useMemo, useCallback และเทคนิคด้านประสิทธิภาพ

ภาพประกอบ React Hooks ขั้นสูงพร้อมรูปแบบการประกอบกันและการเพิ่มประสิทธิภาพ

React Hooks เปลี่ยนแปลงวิธีจัดการ state และ side effect ในคอมโพเนนต์แบบฟังก์ชันอย่างสิ้นเชิง นอกเหนือจากการใช้งานพื้นฐานของ useState และ useEffect รูปแบบขั้นสูงช่วยให้สามารถสร้างโค้ดที่นำกลับมาใช้ใหม่ได้ มีประสิทธิภาพสูง และดูแลรักษาง่าย

ข้อกำหนดเบื้องต้น

คู่มือนี้สมมติว่าผู้อ่านคุ้นเคยกับ Hooks พื้นฐาน (useState, useEffect, useContext) ตัวอย่างใช้ React 18+ และ TypeScript เพื่อการระบุชนิดข้อมูลที่แข็งแกร่ง

เชี่ยวชาญ useEffect: หลีกเลี่ยงกับดักที่พบบ่อย

Hook useEffect มักถูกเข้าใจผิดและใช้งานไม่ถูกต้อง การเข้าใจการทำงานอย่างลึกซึ้งช่วยหลีกเลี่ยงบั๊กที่ตรวจจับได้ยากและปัญหาด้านประสิทธิภาพ

หลักการพื้นฐาน: useEffect ซิงโครไนซ์คอมโพเนนต์กับระบบภายนอก หาก effect ไม่สื่อสารกับโลกภายนอก (API, DOM, timer) ก็มีแนวโน้มว่าจะไม่จำเป็น

hooks/useDocumentTitle.tstsx
// Custom hook to synchronize document title
import { useEffect } from 'react'

export function useDocumentTitle(title: string) {
  useEffect(() => {
    // Save the previous title
    const previousTitle = document.title
    // Update the title
    document.title = title

    // Cleanup: restore previous title on unmount
    return () => {
      document.title = previousTitle
    }
  }, [title]) // Only trigger when title changes
}

ข้อผิดพลาดที่พบบ่อยที่สุดคือการละเลย dependency array หรือใส่ค่าที่ไม่เสถียร ค่าทุกค่าที่ใช้ใน effect ต้องปรากฏใน dependencies

components/UserProfile.tsxtsx
'use client'

import { useEffect, useState } from 'react'

interface User {
  id: string
  name: string
  email: string
}

interface UserProfileProps {
  userId: string
}

export function UserProfile({ userId }: UserProfileProps) {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    // Flag to prevent state updates after unmount
    let isMounted = true
    // Controller to cancel pending requests
    const controller = new AbortController()

    async function fetchUser() {
      setLoading(true)
      setError(null)

      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal
        })

        if (!response.ok) {
          throw new Error('User not found')
        }

        const data = await response.json()

        // Check if component is still mounted
        if (isMounted) {
          setUser(data)
        }
      } catch (err) {
        // Ignore abort errors
        if (err instanceof Error && err.name !== 'AbortError') {
          if (isMounted) {
            setError(err.message)
          }
        }
      } finally {
        if (isMounted) {
          setLoading(false)
        }
      }
    }

    fetchUser()

    // Cleanup: abort request and mark as unmounted
    return () => {
      isMounted = false
      controller.abort()
    }
  }, [userId]) // Re-run effect when userId changes

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error}</div>
  if (!user) return null

  return (
    <div className="p-4 border rounded-lg">
      <h2 className="text-xl font-bold">{user.name}</h2>
      <p className="text-gray-600">{user.email}</p>
    </div>
  )
}

รูปแบบที่ใช้ AbortController และธง isMounted นี้ช่วยป้องกัน memory leak และการอัปเดต state บนคอมโพเนนต์ที่ถูก unmount แล้ว

สร้าง custom hooks ที่นำกลับมาใช้ใหม่ได้

Custom hooks ห่อหุ้มตรรกะที่นำกลับมาใช้ใหม่ได้ custom hook ที่ดียึดหลักความรับผิดชอบเดียวและให้ API ที่ชัดเจน

hooks/useLocalStorage.tstsx
// Generic hook to persist state in localStorage
import { useState, useEffect, useCallback } from 'react'

export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
  // Initialize state with stored value or default
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') {
      return initialValue
    }

    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.warn(`Error reading localStorage "${key}":`, error)
      return initialValue
    }
  })

  // Sync with localStorage on every change
  useEffect(() => {
    if (typeof window === 'undefined') return

    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue))
    } catch (error) {
      console.warn(`Error writing localStorage "${key}":`, error)
    }
  }, [key, storedValue])

  // Function to update value
  const setValue = useCallback((value: T | ((prev: T) => T)) => {
    setStoredValue((prev) => {
      const nextValue = value instanceof Function ? value(prev) : value
      return nextValue
    })
  }, [])

  // Function to remove value
  const removeValue = useCallback(() => {
    setStoredValue(initialValue)
    if (typeof window !== 'undefined') {
      window.localStorage.removeItem(key)
    }
  }, [key, initialValue])

  return [storedValue, setValue, removeValue]
}

สามารถใช้ hook นี้ในคอมโพเนนต์ใดก็ได้เพื่อจัดเก็บข้อมูลให้คงอยู่

components/ThemeToggle.tsxtsx
'use client'

import { useLocalStorage } from '@/hooks/useLocalStorage'

type Theme = 'light' | 'dark' | 'system'

export function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<Theme>('theme', 'system')

  return (
    <select
      value={theme}
      onChange={(e) => setTheme(e.target.value as Theme)}
      className="px-3 py-2 border rounded-lg"
    >
      <option value="light">Light</option>
      <option value="dark">Dark</option>
      <option value="system">System</option>
    </select>
  )
}
ข้อตกลงในการตั้งชื่อ

Custom hooks ขึ้นต้นด้วย "use" เสมอ (useLocalStorage, useFetch, useDebounce) ข้อตกลงนี้ช่วยให้ React ตรวจสอบกฎของ hook ได้และเครื่องมือ linting ทำงานได้อย่างถูกต้อง

รูปแบบการประกอบกันด้วย useReducer

สำหรับ state ที่ซับซ้อนซึ่งมีหลายแอ็กชัน useReducer ให้โครงสร้างที่คาดเดาได้มากกว่าการเรียก useState หลายครั้ง

hooks/useCart.tstsx
// Cart management hook with useReducer
import { useReducer, useCallback, useMemo } from 'react'

interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

interface CartState {
  items: CartItem[]
  isOpen: boolean
}

type CartAction =
  | { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
  | { type: 'REMOVE_ITEM'; payload: string }
  | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
  | { type: 'CLEAR_CART' }
  | { type: 'TOGGLE_CART' }

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingItem = state.items.find(
        (item) => item.id === action.payload.id
      )

      if (existingItem) {
        // Increment quantity if item exists
        return {
          ...state,
          items: state.items.map((item) =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          )
        }
      }

      // Add new item
      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }]
      }
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter((item) => item.id !== action.payload)
      }

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map((item) =>
          item.id === action.payload.id
            ? { ...item, quantity: action.payload.quantity }
            : item
        ).filter((item) => item.quantity > 0)
      }

    case 'CLEAR_CART':
      return { ...state, items: [] }

    case 'TOGGLE_CART':
      return { ...state, isOpen: !state.isOpen }

    default:
      return state
  }
}

const initialState: CartState = {
  items: [],
  isOpen: false
}

export function useCart() {
  const [state, dispatch] = useReducer(cartReducer, initialState)

  // Memoized actions to avoid re-renders
  const addItem = useCallback(
    (item: Omit<CartItem, 'quantity'>) => {
      dispatch({ type: 'ADD_ITEM', payload: item })
    },
    []
  )

  const removeItem = useCallback((id: string) => {
    dispatch({ type: 'REMOVE_ITEM', payload: id })
  }, [])

  const updateQuantity = useCallback((id: string, quantity: number) => {
    dispatch({ type: 'UPDATE_QUANTITY', payload: { id, quantity } })
  }, [])

  const clearCart = useCallback(() => {
    dispatch({ type: 'CLEAR_CART' })
  }, [])

  const toggleCart = useCallback(() => {
    dispatch({ type: 'TOGGLE_CART' })
  }, [])

  // Memoized derived values
  const total = useMemo(
    () => state.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    ),
    [state.items]
  )

  const itemCount = useMemo(
    () => state.items.reduce((sum, item) => sum + item.quantity, 0),
    [state.items]
  )

  return {
    items: state.items,
    isOpen: state.isOpen,
    total,
    itemCount,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    toggleCart
  }
}

วิธีนี้รวมศูนย์ตรรกะของตะกร้าทั้งหมดและทำให้เขียน unit test ได้ง่ายขึ้น

พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การเพิ่มประสิทธิภาพด้วย useMemo และ useCallback

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

components/ProductList.tsxtsx
'use client'

import { useState, useMemo, useCallback, memo } from 'react'

interface Product {
  id: string
  name: string
  price: number
  category: string
  inStock: boolean
}

interface ProductListProps {
  products: Product[]
}

// Memoized child component
const ProductCard = memo(function ProductCard({
  product,
  onAddToCart
}: {
  product: Product
  onAddToCart: (id: string) => void
}) {
  console.log(`Render ProductCard: ${product.name}`)

  return (
    <div className="border rounded-lg p-4">
      <h3 className="font-medium">{product.name}</h3>
      <p className="text-gray-600">${product.price}</p>
      <button
        onClick={() => onAddToCart(product.id)}
        disabled={!product.inStock}
        className="mt-2 px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {product.inStock ? 'Add to Cart' : 'Out of Stock'}
      </button>
    </div>
  )
})

export function ProductList({ products }: ProductListProps) {
  const [filter, setFilter] = useState('')
  const [sortBy, setSortBy] = useState<'name' | 'price'>('name')
  const [cart, setCart] = useState<string[]>([])

  // useMemo: memoize result of expensive computation
  const filteredAndSortedProducts = useMemo(() => {
    console.log('Computing filteredAndSortedProducts')

    let result = products

    // Filter by name
    if (filter) {
      result = result.filter((p) =>
        p.name.toLowerCase().includes(filter.toLowerCase())
      )
    }

    // Sort
    result = [...result].sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name)
      }
      return a.price - b.price
    })

    return result
  }, [products, filter, sortBy]) // Recompute only when these deps change

  // useCallback: memoize a function
  const handleAddToCart = useCallback((productId: string) => {
    setCart((prev) => [...prev, productId])
  }, []) // Empty deps: function never changes

  // Memoized statistics
  const stats = useMemo(
    () => ({
      total: filteredAndSortedProducts.length,
      inStock: filteredAndSortedProducts.filter((p) => p.inStock).length,
      avgPrice:
        filteredAndSortedProducts.reduce((sum, p) => sum + p.price, 0) /
          filteredAndSortedProducts.length || 0
    }),
    [filteredAndSortedProducts]
  )

  return (
    <div>
      <div className="mb-4 flex gap-4">
        <input
          type="text"
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          placeholder="Search..."
          className="px-3 py-2 border rounded-lg"
        />
        <select
          value={sortBy}
          onChange={(e) => setSortBy(e.target.value as 'name' | 'price')}
          className="px-3 py-2 border rounded-lg"
        >
          <option value="name">Name</option>
          <option value="price">Price</option>
        </select>
      </div>

      <p className="text-sm text-gray-600 mb-4">
        {stats.total} products ({stats.inStock} in stock) -
        Average price: ${stats.avgPrice.toFixed(2)}
      </p>

      <div className="grid grid-cols-3 gap-4">
        {filteredAndSortedProducts.map((product) => (
          <ProductCard
            key={product.id}
            product={product}
            onAddToCart={handleAddToCart}
          />
        ))}
      </div>
    </div>
  )
}
เมื่อใดควรใช้ useMemo/useCallback?

Hook เหล่านี้เพิ่มความซับซ้อน ควรสงวนไว้สำหรับ: (1) การคำนวณที่ใช้ทรัพยากรจริงๆ (2) prop ที่ส่งไปยังคอมโพเนนต์ที่ memo ด้วย memo() (3) dependency ของ hook อื่น

Hook debounce สำหรับช่องค้นหา

Debounce ป้องกันไม่ให้แอ็กชันถูกเรียกบ่อยเกินไป โดยทั่วไปใช้ขณะพิมพ์ในช่องค้นหา

hooks/useDebounce.tstsx
// Generic debounce hook
import { useState, useEffect } from 'react'

export function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value)

  useEffect(() => {
    // Create a timer that updates the value after delay
    const timer = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    // Cleanup: cancel timer if value changes before delay
    return () => {
      clearTimeout(timer)
    }
  }, [value, delay])

  return debouncedValue
}
components/SearchInput.tsxtsx
'use client'

import { useState, useEffect } from 'react'
import { useDebounce } from '@/hooks/useDebounce'

interface SearchResult {
  id: string
  title: string
}

export function SearchInput() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<SearchResult[]>([])
  const [loading, setLoading] = useState(false)

  // Debounce the query by 300ms
  const debouncedQuery = useDebounce(query, 300)

  useEffect(() => {
    // Don't search if query is empty
    if (!debouncedQuery.trim()) {
      setResults([])
      return
    }

    async function search() {
      setLoading(true)

      try {
        const response = await fetch(
          `/api/search?q=${encodeURIComponent(debouncedQuery)}`
        )
        const data = await response.json()
        setResults(data.results)
      } catch (error) {
        console.error('Search error:', error)
      } finally {
        setLoading(false)
      }
    }

    search()
  }, [debouncedQuery]) // Only triggered when debouncedQuery changes

  return (
    <div className="relative">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
        className="w-full px-4 py-2 border rounded-lg"
      />

      {loading && (
        <div className="absolute right-3 top-2.5">
          <div className="w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
        </div>
      )}

      {results.length > 0 && (
        <ul className="absolute w-full mt-1 bg-white border rounded-lg shadow-lg">
          {results.map((result) => (
            <li
              key={result.id}
              className="px-4 py-2 hover:bg-gray-100 cursor-pointer"
            >
              {result.title}
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

คอมโพเนนต์จะค้นหาเฉพาะ 300 มิลลิวินาทีหลังจากที่ผู้ใช้หยุดพิมพ์ ลดคำขอที่ไม่จำเป็นจำนวนมาก

useImperativeHandle สำหรับ ref ขั้นสูง

Hook นี้เปิดเผยเมธอดเฉพาะของคอมโพเนนต์ลูกให้คอมโพเนนต์แม่ผ่าน ref

components/VideoPlayer.tsxtsx
'use client'

import {
  useRef,
  useImperativeHandle,
  forwardRef,
  useState,
  useCallback
} from 'react'

// Interface of exposed methods
export interface VideoPlayerRef {
  play: () => void
  pause: () => void
  seek: (time: number) => void
  getCurrentTime: () => number
}

interface VideoPlayerProps {
  src: string
  poster?: string
}

export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
  function VideoPlayer({ src, poster }, ref) {
    const videoRef = useRef<HTMLVideoElement>(null)
    const [isPlaying, setIsPlaying] = useState(false)

    // Expose only desired methods to parent
    useImperativeHandle(
      ref,
      () => ({
        play: () => {
          videoRef.current?.play()
          setIsPlaying(true)
        },
        pause: () => {
          videoRef.current?.pause()
          setIsPlaying(false)
        },
        seek: (time: number) => {
          if (videoRef.current) {
            videoRef.current.currentTime = time
          }
        },
        getCurrentTime: () => {
          return videoRef.current?.currentTime ?? 0
        }
      }),
      [] // No deps: methods always use current ref
    )

    const handlePlayPause = useCallback(() => {
      if (isPlaying) {
        videoRef.current?.pause()
      } else {
        videoRef.current?.play()
      }
      setIsPlaying(!isPlaying)
    }, [isPlaying])

    return (
      <div className="relative">
        <video
          ref={videoRef}
          src={src}
          poster={poster}
          className="w-full rounded-lg"
          onPlay={() => setIsPlaying(true)}
          onPause={() => setIsPlaying(false)}
        />
        <button
          onClick={handlePlayPause}
          className="absolute bottom-4 left-4 px-4 py-2 bg-black/50 text-white rounded"
        >
          {isPlaying ? 'Pause' : 'Play'}
        </button>
      </div>
    )
  }
)
components/VideoController.tsxtsx
'use client'

import { useRef } from 'react'
import { VideoPlayer, VideoPlayerRef } from './VideoPlayer'

export function VideoController() {
  const playerRef = useRef<VideoPlayerRef>(null)

  const handleSkipForward = () => {
    if (playerRef.current) {
      const currentTime = playerRef.current.getCurrentTime()
      playerRef.current.seek(currentTime + 10)
    }
  }

  return (
    <div className="space-y-4">
      <VideoPlayer
        ref={playerRef}
        src="/videos/demo.mp4"
        poster="/images/poster.jpg"
      />

      <div className="flex gap-2">
        <button
          onClick={() => playerRef.current?.play()}
          className="px-4 py-2 bg-green-600 text-white rounded"
        >
          Play
        </button>
        <button
          onClick={() => playerRef.current?.pause()}
          className="px-4 py-2 bg-red-600 text-white rounded"
        >
          Pause
        </button>
        <button
          onClick={handleSkipForward}
          className="px-4 py-2 bg-blue-600 text-white rounded"
        >
          +10s
        </button>
      </div>
    </div>
  )
}

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

Hook จัดการวงจรชีวิตของ request

Hook ขั้นสูงสำหรับจัดการสถานะ loading, error และข้อมูลด้วยการระบุชนิดข้อมูลที่เข้มงวด

hooks/useFetch.tstsx
// Generic hook for HTTP requests
import { useState, useEffect, useCallback, useRef } from 'react'

interface UseFetchState<T> {
  data: T | null
  loading: boolean
  error: Error | null
}

interface UseFetchOptions {
  immediate?: boolean // Execute immediately on mount
  onSuccess?: <T>(data: T) => void
  onError?: (error: Error) => void
}

export function useFetch<T>(
  url: string,
  options: UseFetchOptions = {}
) {
  const { immediate = true, onSuccess, onError } = options

  const [state, setState] = useState<UseFetchState<T>>({
    data: null,
    loading: immediate,
    error: null
  })

  // Ref to prevent updates after unmount
  const mountedRef = useRef(true)
  // Ref for abort controller
  const abortControllerRef = useRef<AbortController | null>(null)

  const execute = useCallback(async () => {
    // Abort previous request if in progress
    abortControllerRef.current?.abort()
    abortControllerRef.current = new AbortController()

    setState((prev) => ({ ...prev, loading: true, error: null }))

    try {
      const response = await fetch(url, {
        signal: abortControllerRef.current.signal
      })

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      const data: T = await response.json()

      if (mountedRef.current) {
        setState({ data, loading: false, error: null })
        onSuccess?.(data)
      }

      return data
    } catch (error) {
      // Ignore abort errors
      if (error instanceof Error && error.name === 'AbortError') {
        return null
      }

      const err = error instanceof Error ? error : new Error('Unknown error')

      if (mountedRef.current) {
        setState({ data: null, loading: false, error: err })
        onError?.(err)
      }

      return null
    }
  }, [url, onSuccess, onError])

  const reset = useCallback(() => {
    setState({ data: null, loading: false, error: null })
  }, [])

  // Execute on mount if immediate=true
  useEffect(() => {
    if (immediate) {
      execute()
    }

    return () => {
      mountedRef.current = false
      abortControllerRef.current?.abort()
    }
  }, [execute, immediate])

  return {
    ...state,
    execute,
    reset,
    isIdle: !state.loading && !state.data && !state.error
  }
}

Hook นี้มอบ API ที่ครบถ้วนสำหรับจัดการคำขอใดๆ พร้อมการยกเลิกอัตโนมัติและการจัดการวงจรชีวิต

พร้อมที่จะพิชิตการสัมภาษณ์ React / Next.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

สรุป

รูปแบบ React Hooks ขั้นสูงช่วยให้สามารถสร้างโค้ดที่ดูแลรักษาง่ายและมีประสิทธิภาพสูง ประเด็นสำคัญที่ควรจดจำ:

  • useEffect: ใส่ cleanup เสมอและจัดการ race condition ด้วย AbortController
  • Custom hooks: ห่อหุ้มตรรกะที่นำกลับมาใช้ใหม่ได้ด้วย API ที่ชัดเจน
  • useReducer: เลือกใช้สำหรับ state ที่ซับซ้อนซึ่งมีหลายแอ็กชัน
  • useMemo/useCallback: ใช้อย่างมีจุดประสงค์เฉพาะเมื่อจำเป็น
  • useDebounce: จำกัดการเรียกที่ถี่เกินไปในการค้นหา
  • useImperativeHandle: เปิดเผยเมธอดแบบสั่งการอย่างมีการควบคุม

ความเชี่ยวชาญในรูปแบบเหล่านี้สร้างความแตกต่างระหว่างนักพัฒนา React ระดับกลางกับผู้เชี่ยวชาญ Hook แต่ละตัวแก้ปัญหาเฉพาะ: การเลือกเครื่องมือที่ถูกต้องในเวลาที่ถูกต้องคือกุญแจสู่สถาปัตยกรรม React ที่แข็งแกร่ง

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#react hooks
#useEffect
#custom hooks
#performance
#react patterns

แชร์

บทความที่เกี่ยวข้อง