Back to blog

Mastering React Hooks: Beyond useState and useEffect

June 18, 2024 (1y ago)

React hooks revolutionized how we write React components. While most developers know useState and useEffect, there's a rich ecosystem of hooks that can dramatically improve your code.

React Hooks were introduced in React 16.8 (2019) and have since become the standard way to write React components.

Custom Hooks: The Real Power

Custom hooks are the key to reusable logic in React. Here are some patterns I've found invaluable:

1. useLocalStorage Hook

Essential Hook - Persist state across browser sessions:

function useLocalStorage<T>(key: string, initialValue: T) {
  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.error(error);
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue] as const;
}

2. useDebounce Hook

Performance Hook - Delay expensive operations:

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

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

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

  return debouncedValue;
}

Advanced Hook Patterns

useAsync Hook

Handle async operations with loading states:

function useAsync<T, E = string>(
  asyncFunction: () => Promise<T>,
  immediate = true
) {
  const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
  const [value, setValue] = useState<T | null>(null);
  const [error, setError] = useState<E | null>(null);

  const execute = useCallback(async () => {
    setStatus('pending');
    setValue(null);
    setError(null);

    try {
      const response = await asyncFunction();
      setValue(response);
      setStatus('success');
    } catch (error) {
      setError(error as E);
      setStatus('error');
    }
  }, [asyncFunction]);

  useEffect(() => {
    if (immediate) {
      execute();
    }
  }, [execute, immediate]);

  return { execute, status, value, error };
}

usePrevious Hook

Track previous values:

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

Hook Composition

The real power comes from combining hooks:

function useUserData(userId: string) {
  const { value: user, status, error } = useAsync(() => 
    fetchUser(userId)
  );

  const [settings, setSettings] = useLocalStorage('userSettings', {});
  const debouncedSettings = useDebounce(settings, 500);

  return {
    user,
    isLoading: status === 'pending',
    error,
    settings: debouncedSettings,
    updateSettings: setSettings
  };
}

Pro tip: Always start hooks with "use" and follow the rules of hooks - only call them at the top level!

Performance Optimizations

useMemo and useCallback

Performance - Prevent unnecessary re-renders:

function ExpensiveComponent({ data, onUpdate }) {
  const expensiveValue = useMemo(() => {
    return data.reduce((sum, item) => sum + item.value, 0);
  }, [data]);

  const handleClick = useCallback(() => {
    onUpdate(expensiveValue);
  }, [expensiveValue, onUpdate]);

  return <div onClick={handleClick}>Total: {expensiveValue}</div>;
}

Best Practices

  1. Rule 1 - Always start custom hooks with "use"
  2. Rule 2 - Keep hooks small and focused
  3. Rule 3 - Use TypeScript for better type safety
  4. Rule 4 - Document your custom hooks
  5. Rule 5 - Test hooks independently

Real-World Examples

useApi Hook

function useApi<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(url)
      .then(response => response.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [url]);

  return { data, loading, error };
}

Conclusion

Mastering React hooks goes beyond knowing the basics. Custom hooks allow you to:

  • Extract reusable logic
  • Improve code organization
  • Enhance performance
  • Create better abstractions

Start building your own hook library and watch your React applications become more maintainable and powerful!


What custom hooks have you built? Share your favorites in the comments!

Share this post