ReactJavaScriptFrontendHooks

Mastering React Hooks: Beyond the Basics

Alex ReedFebruary 28, 20244 min read

Mastering React Hooks: Beyond the Basics

React Hooks revolutionized how we write components, but with great power comes great responsibility. Let's explore advanced patterns that will make your code more maintainable and performant.

Understanding the Hook Rules

Before diving deep, let's reinforce the fundamentals:

  1. Only call hooks at the top level - No hooks inside loops, conditions, or nested functions
  2. Only call hooks from React functions - Components or custom hooks

These rules exist because React relies on the order of hook calls to maintain state.

useEffect: The Swiss Army Knife

Dependency Array Deep Dive

The dependency array is where most bugs originate:

useEffect(() => {
  fetchUser(userId);
}, [userId]); // ✅ Only re-run when userId changes

Common mistake: forgetting to include all dependencies:

const [count, setCount] = useState(0);

useEffect(() => {
  console.log(count);
  // Stale closure if count not in dependencies
}, []); // ❌ Missing dependency

Cleanup Functions

Proper cleanup prevents memory leaks:

useEffect(() => {
  const subscription = dataSource.subscribe();
  
  return () => {
    subscription.unsubscribe();
  };
}, [dataSource]);

useMemo and useCallback: When to Use Them

The Golden Rule

Don't optimize prematurely. Only use these hooks when:

  1. You're passing callbacks to optimized child components
  2. You're computing expensive values
  3. You're breaking referential equality

Practical Example

const ProductList = ({ products, filter }) => {
  // Only recalculate when products or filter change
  const filteredProducts = useMemo(() => {
    return products.filter(p => 
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]);

  // Stable reference for child components
  const handleAddToCart = useCallback((product) => {
    addToCart(product);
  }, []);

  return (
    <ul>
      {filteredProducts.map(product => (
        <ProductItem 
          key={product.id} 
          product={product}
          onAddToCart={handleAddToCart}
        />
      ))}
    </ul>
  );
};

Custom Hooks: Encapsulating Logic

Custom hooks are the secret to clean, reusable code:

useLocalStorage Hook

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');

useFetch Hook

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err);
          setLoading(false);
        }
      });

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

Common Pitfalls and Solutions

1. Stale Closures

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      // ❌ Always logs 0
      console.log(count);
    }, 1000);

    return () => clearInterval(timer);
  }, []); // Missing dependency

  // ✅ Use functional update
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []);
}

2. Infinite Loops

// ❌ Infinite loop
useEffect(() => {
  setState(someValue);
}, [someValue]);

// ✅ Use ref for values that shouldn't trigger updates
const someRef = useRef(someValue);

Performance Optimization Patterns

1. Memoizing Components

const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
  return (
    <div>
      {/* Expensive rendering */}
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparison logic
  return prevProps.data.id === nextProps.data.id;
});

2. Virtual Lists for Large Datasets

import { useVirtual } from 'react-virtual';

function VirtualList({ items }) {
  const parentRef = useRef();
  const rowVirtualizer = useVirtual({
    size: items.length,
    parentRef,
    estimateSize: () => 50,
  });

  return (
    <div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
      <div style={{ height: rowVirtualizer.totalSize }}>
        {rowVirtualizer.virtualItems.map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: virtualRow.size,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            {items[virtualRow.index]}
          </div>
        ))}
      </div>
    </div>
  );
}

Conclusion

Mastering hooks requires understanding both the mechanics and the mental model. Remember:

  • Keep hooks simple and focused
  • Profile before optimizing
  • Share logic through custom hooks
  • Always clean up side effects

The best React code is often the simplest React code.


What's your favorite custom hook? Let me know in the comments!

Enjoyed this article?

Read more posts

View All Posts