Skip to content
All essays
WebMarch 20, 202410 min

React Hooks Best Practices

Master React Hooks with these essential best practices for writing clean, maintainable, and performant code

Ü
Ümit Uz
Mobile & Full Stack Developer

React Hooks have revolutionized how we write React components, but they come with their own set of best practices that every developer should follow. Let's dive into the essential patterns that will make your Hooks code cleaner and more maintainable.

Understanding the Rules of Hooks

The two fundamental rules of Hooks are: only call Hooks at the top level, and only call Hooks from React functions. These rules exist because Hooks rely on call order to work correctly. Breaking these rules leads to bugs that are difficult to diagnose.

Always Call Hooks at the Top Level

Never call Hooks inside loops, conditions, or nested functions. Hooks should always be called at the top level of your React function, before any early returns. This ensures Hooks are called in the same order on every render, which is essential for React to properly preserve state between calls.

typescript
// ❌ Bad: Conditional Hook
if (condition) {
  useEffect(() => {
    // ...
  }, []);
}

// ✅ Good: Always called
useEffect(() => {
  if (condition) {
    // ...
  }
}, []);

useState Best Practices

When using useState, keep your state simple and granular. Don't try to store everything in a single state object unless the values are truly related. Multiple state variables are often easier to manage and update.

Functional Updates

When your new state depends on the previous state, use the functional update form. This prevents stale closure issues and ensures you're always working with the latest state.

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

// ✅ Good: Functional update
setCount(prev => prev + 1);

// ❌ Bad: May use stale value
setCount(count + 1);

Lazy Initialization

For expensive initial state calculations, use lazy initialization with a function. This function runs only once during initial render, not on every re-render.

typescript
const [data, setData] = useState(() => {
  return expensiveCalculation();
});

useEffect Patterns

The useEffect Hook is where side effects live, but it's also where many bugs originate. Understanding dependencies is crucial for correct usage.

The Dependency Array

Always include all values from the component scope that are used inside the effect. React's ESLint plugin will warn you about missing dependencies, and you should heed these warnings.

typescript
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    subscription.unsubscribe();
  };
}, [props.source]); // ✅ Include all dependencies

Cleanup Functions

Always return a cleanup function from useEffect when you have subscriptions, timers, or any side effect that needs cleanup. This prevents memory leaks and ensures proper resource management.

typescript
useEffect(() => {
  const timer = setInterval(() => {
    console.log('Timer tick');
  }, 1000);

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

Separate Concerns

Don't try to do too much in a single useEffect. Split effects by concern rather than lifecycle method. This makes your code more readable and easier to maintain.

Custom Hooks

Custom Hooks are the key to reusing stateful logic. Extract common logic into custom Hooks to reduce duplication and improve code organization.

Naming Conventions

Always prefix custom Hooks with "use" to clearly indicate they follow the Rules of Hooks. This also enables ESLint to apply the exhaustive-deps rule to your custom Hooks.

typescript
// ✅ Good: Follows naming convention
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });
  // ...
  return size;
}

Single Responsibility

Each custom Hook should have a single, well-defined responsibility. If your Hook is doing multiple unrelated things, consider splitting it into multiple Hooks.

useCallback and useMemo

These Hooks are optimization tools, not something you should use everywhere. Use them only when you've identified a performance problem through profiling.

When to Use useCallback

Use useCallback when passing callbacks to optimized child components or when a callback is a dependency in other Hooks. Don't wrap every function in useCallback by default.

typescript
const handleClick = useCallback(() => {
  doSomething(dependency);
}, [dependency]);

When to Use useMemo

Use useMemo for expensive calculations that you don't want to repeat on every render. Be careful not to optimize prematurely—measure first, then optimize.

typescript
const expensiveValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

useContext Patterns

Context is great for sharing global state, but it can cause unnecessary re-renders if not used carefully. Split your context when you have values that change at different rates.

Separate Contexts

Instead of one large context object, create separate contexts for different concerns. This prevents components from re-rendering when unrelated values change.

typescript
// ✅ Good: Separate contexts
const UserContext = createContext<UserContextValue>(null);
const ThemeContext = createContext<ThemeContextValue>(null);

Memoize Context Values

Memoize context values to prevent unnecessary re-renders of consumers. This is especially important for objects and arrays created in the parent.

useRef Use Cases

useRef is more than just a way to access DOM elements. It's perfect for storing any mutable value that doesn't trigger re-renders.

Storing Previous Values

A common pattern is tracking previous props or state values using useRef.

typescript
function usePrevious(value: T) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

Testing Hooks

When testing custom Hooks, use @testing-library/react-hooks. This library provides utilities for testing Hooks in isolation without needing to create wrapper components.

Conclusion

React Hooks provide a powerful way to write component logic, but they require understanding their rules and best practices. Follow these guidelines to write cleaner, more maintainable code. Remember: the rules of Hooks exist for a reason, and breaking them leads to bugs that are difficult to track down.

Start with simple patterns, and gradually adopt more advanced techniques as you become comfortable with Hooks basics. The key is to understand why these best practices exist rather than just following them blindly.

Related essays

Next essay
Modern CSS Features Guide