Menu
📱 Lihat versi lengkap (non-AMP)
React Development JavaScript Frameworks Frontend Development

Tutorial React Hooks Lengkap dari useState hingga Custom Hooks Advanced untuk Tahun 2026

Editor: Hendra WIjaya
Update: 15 January 2026
Baca: 12 menit

Evolusi React Hooks di Tahun 2026

Sejak diperkenalkan di React 16.8, React Hooks telah merevolusi cara kita menulis functional components. Di tahun 2026, hooks telah menjadi fundamental building block dalam React development ecosystem, dengan puluhan custom hooks dan patterns yang telah matang dan menjadi industry standards.

React Hooks memungkinkan kita untuk menggunakan state dan lifecycle features dalam functional components tanpa perlu class components. Ini bukan sekadar syntax sugar, melainkan paradigma shift yang memungkinkan kode yang lebih reusable, testable, dan maintainable.

Studi terbaru menunjukkan bahwa 92% React developers lebih memilih hooks dibandingkan class components untuk new projects, dengan alasan utama: better code organization, easier testing, dan superior performance.

Fundamental Hooks: Dasar-dasar yang Wajib Dikuasai

useState Hook: State Management Simplified

useState adalah hook paling fundamental yang memungkinkan functional components untuk memiliki local state.

import { useState } from 'react';

function Counter() {
  // Basic useState dengan primitive value
  const [count, setCount] = useState(0);

  // useState dengan object state
  const [user, setUser] = useState({
    name: 'John Doe',
    email: 'john@example.com',
    age: 30
  });

  // useState dengan array
  const [todos, setTodos] = useState([]);

  // useState dengan lazy initialization
  const [expensiveState, setExpensiveState] = useState(() => {
    return calculateExpensiveValue(); // Hanya dijalankan sekali
  });

  const increment = () => {
    // Functional update untuk prev state dependency
    setCount(prevCount => prevCount + 1);
  };

  const updateUserName = (newName) => {
    // Immutable update untuk object state
    setUser(prevUser => ({
      ...prevUser,
      name: newName
    }));
  };

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

Advanced useState patterns:

// State reducer pattern untuk complex state
const [state, dispatch] = useReducer((state, action) => {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.field]: action.value };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
}, initialState);

// Custom hook untuk object state management
function useObjectState(initialState) {
  const [state, setState] = useState(initialState);

  const updateField = (field, value) => {
    setState(prev => ({ ...prev, [field]: value }));
  };

  const resetState = () => {
    setState(initialState);
  };

  return [state, updateField, resetState];
}

useEffect Hook: Side Effects Management

useEffect menangani side effects seperti data fetching, subscriptions, dan DOM manipulations.

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Effect dengan dependency array
  useEffect(() => {
    let isCancelled = false;

    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();

        if (!isCancelled) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message);
          setUser(null);
        }
      } finally {
        if (!isCancelled) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // Cleanup function
    return () => {
      isCancelled = true;
    };
  }, [userId]); // Re-run hanya jika userId berubah

  // Effect tanpa dependencies (runs every render)
  useEffect(() => {
    document.title = user ? user.name : 'Loading...';
  });

  // Effect dengan empty dependency array (runs once)
  useEffect(() => {
    console.log('Component mounted');
    return () => {
      console.log('Component unmounted');
    };
  }, []);

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

  return <div>{user.name}</div>;
}

useContext Hook: Global State Management

useContext menyediakan cara untuk share data antar components tanpa prop drilling.

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

// Create context
const ThemeContext = createContext();

// Theme provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

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

  const value = {
    theme,
    toggleTheme,
    colors: theme === 'light' ? {
      background: '#ffffff',
      text: '#000000'
    } : {
      background: '#000000',
      text: '#ffffff'
    }
  };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// Custom hook untuk theme consumption
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Consumer component
function ThemeButton() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      onClick={toggleTheme}
      style={{ background: theme === 'light' ? '#333' : '#fff' }}
    >
      Current theme: {theme}
    </button>
  );
}

Advanced Hooks untuk Performance Optimization

useMemo Hook: Expensive Calculations Caching

useMemo meng-expensive calculations dan cached hasilnya untuk performance optimization.

import { useState, useMemo } from 'react';

function ExpensiveCalculation({ numbers }) {
  const [filter, setFilter] = useState('');

  // Memoized expensive calculation
  const sortedNumbers = useMemo(() => {
    console.log('Running expensive sort...');
    return numbers
      .filter(num => num > 0)
      .sort((a, b) => a - b)
      .slice(0, 100); // Limit hasil untuk performance
  }, [numbers]);

  // Memoized filtered results
  const filteredNumbers = useMemo(() => {
    console.log('Filtering numbers...');
    return sortedNumbers.filter(num =>
      num.toString().includes(filter)
    );
  }, [sortedNumbers, filter]);

  // Memoized calculated statistics
  const statistics = useMemo(() => {
    if (filteredNumbers.length === 0) return null;

    const sum = filteredNumbers.reduce((acc, num) => acc + num, 0);
    const mean = sum / filteredNumbers.length;
    const median = calculateMedian(filteredNumbers);
    const std = calculateStandardDeviation(filteredNumbers);

    return { sum, mean, median, std };
  }, [filteredNumbers]);

  return (
    <div>
      <input
        type="text"
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter numbers..."
      />
      {statistics && (
        <div>
          <p>Sum: {statistics.sum}</p>
          <p>Mean: {statistics.mean.toFixed(2)}</p>
          <p>Median: {statistics.median}</p>
          <p>Std Dev: {statistics.std.toFixed(2)}</p>
        </div>
      )}
      <ul>
        {filteredNumbers.map(num => (
          <li key={num}>{num}</li>
        ))}
      </ul>
    </div>
  );
}

useCallback Hook: Function Reference Stabilization

useCallback memoizes functions untuk prevent unnecessary re-renders.

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

// Memoized child component
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
  console.log(`Rendering TodoItem: ${todo.id}`);

  return (
    <div>
      <span
        style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
        onClick={() => onToggle(todo.id)}
      >
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
});

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React Hooks', completed: false },
    { id: 2, text: 'Build awesome app', completed: true }
  ]);
  const [filter, setFilter] = useState('all');

  // Memoized event handlers
  const toggleTodo = useCallback((id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  const deleteTodo = useCallback((id) => {
    setTodos(prevTodos =>
      prevTodos.filter(todo => todo.id !== id)
    );
  }, []);

  const addTodo = useCallback((text) => {
    setTodos(prevTodos => [
      ...prevTodos,
      { id: Date.now(), text, completed: false }
    ]);
  }, []);

  // Memoized filtered todos
  const filteredTodos = useMemo(() => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }, [todos, filter]);

  return (
    <div>
      <TodoForm onAdd={addTodo} />
      <FilterButtons currentFilter={filter} setFilter={setFilter} />
      {filteredTodos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={toggleTodo}
          onDelete={deleteTodo}
        />
      ))}
    </div>
  );
}

useReducer Hook: Complex State Management

useReducer ideal untuk complex state logic yang melibatkan multiple sub-values atau next state bergantung pada previous state.

import { useReducer } from 'react';

// Reducer function untuk shopping cart
const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      const existingItem = state.items.find(item => item.id === action.payload.id);

      if (existingItem) {
        return {
          ...state,
          items: state.items.map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          )
        };
      } else {
        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
        )
      };

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

    case 'APPLY_COUPON':
      return {
        ...state,
        coupon: action.payload,
        discount: calculateDiscount(action.payload, state.subtotal)
      };

    default:
      return state;
  }
};

// Cart provider component
function CartProvider({ children }) {
  const initialState = {
    items: [],
    subtotal: 0,
    discount: 0,
    total: 0,
    coupon: null
  };

  const [state, dispatch] = useReducer(cartReducer, initialState);

  // Calculate totals whenever items or discount changes
  const totals = useMemo(() => {
    const subtotal = state.items.reduce((sum, item) =>
      sum + (item.price * item.quantity), 0
    );
    const total = Math.max(0, subtotal - state.discount);

    return { subtotal, total };
  }, [state.items, state.discount]);

  const cartActions = {
    addItem: (item) => dispatch({ type: 'ADD_ITEM', payload: item }),
    removeItem: (itemId) => dispatch({ type: 'REMOVE_ITEM', payload: itemId }),
    updateQuantity: (itemId, quantity) =>
      dispatch({ type: 'UPDATE_QUANTITY', payload: { id: itemId, quantity } }),
    clearCart: () => dispatch({ type: 'CLEAR_CART' }),
    applyCoupon: (couponCode) =>
      dispatch({ type: 'APPLY_COUPON', payload: couponCode })
  };

  const value = {
    ...state,
    ...totals,
    ...cartActions
  };

  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

Custom Hooks: Reusable Logic Abstraction

Building Custom Hooks Best Practices

Custom hooks adalah JavaScript functions yang namanya diawali dengan “use” dan dapat memanggil hooks lain.

// Custom hook untuk data fetching
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isCancelled = false;

    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url, options);

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

        const result = await response.json();

        if (!isCancelled) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message);
          setData(null);
        }
      } finally {
        if (!isCancelled) {
          setLoading(false);
        }
      }
    };

    fetchData();

    return () => {
      isCancelled = true;
    };
  }, [url]);

  const refetch = useCallback(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url, options);
        const result = await response.json();
        setData(result);
        setError(null);
      } catch (err) {
        setError(err.message);
        setData(null);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url, options]);

  return { data, loading, error, refetch };
}

// Usage example
function UserList() {
  const { data: users, loading, error, refetch } = useFetch('/api/users');

  if (loading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <button onClick={refetch}>Refresh</button>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

Advanced Custom Hooks Patterns

1. useLocalStorage Hook:

function useLocalStorage(key, initialValue) {
  // Get initial value from localStorage or use default
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // Function to update value in localStorage and state
  const setValue = useCallback((value) => {
    try {
      // Allow value to be a function so we have the same API as useState
      const valueToStore = value instanceof Function ? value(storedValue) : value;

      setStoredValue(valueToStore);

      // Save to localStorage
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, storedValue]);

  // Function to remove item from localStorage
  const removeValue = useCallback(() => {
    try {
      setStoredValue(initialValue);
      if (typeof window !== 'undefined') {
        window.localStorage.removeItem(key);
      }
    } catch (error) {
      console.error(`Error removing localStorage key "${key}":`, error);
    }
  }, [key, initialValue]);

  return [storedValue, setValue, removeValue];
}

// Usage
function ThemeChanger() {
  const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light');

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
      <button onClick={removeTheme}>
        Reset to Default
      </button>
    </div>
  );
}

2. useDebounce Hook:

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage untuk search input
function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  const { data: searchResults } = useFetch(
    `/api/search?q=${encodeURIComponent(debouncedSearchTerm)}`
  );

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <SearchResults results={searchResults} />
    </div>
  );
}

3. useInfiniteScroll Hook:

function useInfiniteScroll(fetchMore, hasMore) {
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const handleScroll = () => {
      if (loading || !hasMore) return;

      const { scrollTop, clientHeight, scrollHeight } = document.documentElement;

      if (scrollTop + clientHeight >= scrollHeight - 1000) {
        setLoading(true);
        fetchMore().finally(() => setLoading(false));
      }
    };

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [loading, hasMore, fetchMore]);

  return loading;
}

// Usage
function InfinitePostList() {
  const [posts, setPosts] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

  const loadMorePosts = useCallback(async () => {
    const newPosts = await fetchPosts(page);

    if (newPosts.length === 0) {
      setHasMore(false);
    } else {
      setPosts(prevPosts => [...prevPosts, ...newPosts]);
      setPage(prevPage => prevPage + 1);
    }
  }, [page]);

  const loading = useInfiniteScroll(loadMorePosts, hasMore);

  return (
    <div>
      {posts.map(post => (
        <Post key={post.id} post={post} />
      ))}
      {loading && <div>Loading more posts...</div>}
      {!hasMore && <div>No more posts</div>}
    </div>
  );
}

Performance Optimization Strategies

React DevTools Profiler Integration

Hooking into React DevTools untuk performance monitoring:

import { Profiler } from 'react';

function ProfiledComponent({ children, id }) {
  const onRender = (id, phase, actualDuration, baseDuration, startTime, commitTime) => {
    console.log('Performance metrics:', {
      id,
      phase, // "mount" or "update"
      actualDuration, // Time spent rendering
      baseDuration, // Estimated time without memoization
      startTime,
      commitTime
    });

    // Send metrics ke monitoring service
    if (actualDuration > 100) { // Threshold dalam ms
      sendPerformanceAlert({
        componentId: id,
        duration: actualDuration,
        phase
      });
    }
  };

  return (
    <Profiler id={id} onRender={onRender}>
      {children}
    </Profiler>
  );
}

// Usage
function App() {
  return (
    <div>
      <ProfiledComponent id="UserProfile">
        <UserProfile />
      </ProfiledComponent>
      <ProfiledComponent id="TodoList">
        <TodoList />
      </ProfiledComponent>
    </div>
  );
}

Virtual Scrolling dengan Hooks

Optimized rendering untuk large datasets:

function useVirtualScroll(items, containerHeight, itemHeight) {
  const [scrollTop, setScrollTop] = useState(0);

  const visibleStart = Math.floor(scrollTop / itemHeight);
  const visibleEnd = Math.min(
    visibleStart + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  );

  const visibleItems = items.slice(visibleStart, visibleEnd);
  const offsetY = visibleStart * itemHeight;

  const handleScroll = useCallback((e) => {
    setScrollTop(e.target.scrollTop);
  }, []);

  return {
    visibleItems,
    offsetY,
    handleScroll
  };
}

// Virtual scroll component
function VirtualList({ items, containerHeight, itemHeight }) {
  const { visibleItems, offsetY, handleScroll } = useVirtualScroll(
    items,
    containerHeight,
    itemHeight
  );

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div
              key={item.id}
              style={{
                height: itemHeight,
                borderBottom: '1px solid #eee'
              }}
            >
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Testing Hooks dengan React Testing Library

Unit Testing Custom Hooks

Testing patterns untuk custom hooks:

import { renderHook, act } from '@testing-library/react';
import { useFetch } from './useFetch';

// Mock fetch
global.fetch = jest.fn();

describe('useFetch', () => {
  beforeEach(() => {
    fetch.mockClear();
  });

  test('should return data on successful fetch', async () => {
    const mockData = { id: 1, name: 'Test User' };
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockData
    });

    const { result } = renderHook(() => useFetch('/api/user/1'));

    expect(result.current.loading).toBe(true);

    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0));
    });

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBe(null);
  });

  test('should handle fetch error', async () => {
    const errorMessage = 'Network error';
    fetch.mockRejectedValueOnce(new Error(errorMessage));

    const { result } = renderHook(() => useFetch('/api/user/1'));

    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0));
    });

    expect(result.current.loading).toBe(false);
    expect(result.current.error).toBe(errorMessage);
    expect(result.current.data).toBe(null);
  });

  test('should refetch data when refetch is called', async () => {
    fetch
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ id: 1, name: 'User 1' })
      })
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ id: 1, name: 'User 2' })
      });

    const { result } = renderHook(() => useFetch('/api/user/1'));

    await act(async () => {
      await new Promise(resolve => setTimeout(resolve, 0));
    });

    expect(result.current.data.name).toBe('User 1');

    await act(async () => {
      result.current.refetch();
    });

    expect(result.current.data.name).toBe('User 2');
  });
});

Kesimpulan dan Best Practices untuk 2026

React Hooks telah menjadi fundamental building block dalam modern React development. Dengan memahami patterns dan best practices ini, Anda dapat build aplikasi yang performant, maintainable, dan scalable.

Key Takeaways:

  1. Start with basic hooks - Master useState, useEffect, dan useContext dulu
  2. Use useMemo dan useCallback judiciously - Profile dulu sebelum optimizing
  3. Build custom hooks - Abstract reusable logic untuk code organization
  4. Test your hooks - Use React Testing Library untuk comprehensive testing
  5. Monitor performance - Use DevTools Profiler untuk optimization decisions

Future Outlook:

  • Concurrent Features - useTransition dan useDeferredValue akan semakin penting
  • Server Components - New patterns untuk server-side rendering dengan hooks
  • AI-Powered Development - Tools untuk automated hook optimization
  • Better DevTools - Enhanced debugging dan performance monitoring

Dengan menguasai React Hooks patterns di tahun 2026, Anda akan siap untuk menghadapi tantangan development yang lebih kompleks dan build aplikasi yang truly performant dan maintainable.

Bagikan:

Link Postingan: https://www.tirinfo.com/tutorial-react-hooks-lengkap-usestate-custom-hooks-advanced-2026/