⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

React Hooks Best Practices: Writing Clean and Efficient Code

February 15, 2021
reacthooksjavascriptfrontend
React Hooks Best Practices: Writing Clean and Efficient Code

React Hooks Best Practices: Writing Clean and Efficient Code

React Hooks have transformed how we write React components. This guide covers best practices to help you write cleaner, more efficient code.

Understanding the Rules of Hooks

Before diving into best practices, remember the two fundamental rules:

  1. Only call hooks at the top level - Don't call hooks inside loops, conditions, or nested functions
  2. Only call hooks from React functions - Call them from React function components or custom hooks

useState Best Practices

Lazy Initialization

For expensive initial state calculations, use a function:

// Bad - runs on every render
const [state, setState] = useState(expensiveComputation());

// Good - runs only once
const [state, setState] = useState(() => expensiveComputation());

Functional Updates

When new state depends on previous state:

// Bad - can lead to stale state
const increment = () => setCount(count + 1);

// Good - always uses latest state
const increment = () => setCount(prev => prev + 1);

Managing Complex State

// For related data, use a single object
const [user, setUser] = useState({
  name: '',
  email: '',
  age: 0
});

// Update with spread operator
const updateName = (name) => {
  setUser(prev => ({ ...prev, name }));
};

// For independent state, use multiple useState calls
const [name, setName] = useState('');
const [email, setEmail] = useState('');

useEffect Best Practices

Dependency Array

Always include all dependencies:

// Bad - missing dependency
useEffect(() => {
  fetchData(userId);
}, []); // userId should be in deps

// Good
useEffect(() => {
  fetchData(userId);
}, [userId]);

Cleanup Function

Always clean up side effects:

useEffect(() => {
  const controller = new AbortController();
  
  fetch(url, { signal: controller.signal })
    .then(res => res.json())
    .then setData;

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

Separate Concerns

Use multiple effects for unrelated logic:

// Good - separate effects for different concerns
useEffect(() => {
  document.title = `Count: ${count}`;
}, [count]);

useEffect(() => {
  const interval = setInterval(() => {
    console.log('Tick');
  }, 1000);
  return () => clearInterval(interval);
}, []);

useMemo and useCallback

When to Use useMemo

// Use for expensive calculations
const sortedItems = useMemo(() => {
  return items.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

// Don't overuse - simple calculations don't need memoization
const total = items.reduce((sum, item) => sum + item.price, 0);

When to Use useCallback

// Use when passing callbacks to optimized child components
const handleClick = useCallback((id) => {
  setSelected(id);
}, []);

// Use when callback is a dependency
const fetchData = useCallback(async () => {
  const result = await api.get(id);
  setData(result);
}, [id]);

useEffect(() => {
  fetchData();
}, [fetchData]);

Custom Hooks

Extract reusable logic into custom hooks:

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

  const setValue = useCallback((value) => {
    setStoredValue(prev => {
      const valueToStore = value instanceof Function ? value(prev) : value;
      localStorage.setItem(key, JSON.stringify(valueToStore));
      return valueToStore;
    });
  }, [key]);

  return [storedValue, setValue];
}

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

useReducer for Complex State

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'RESET':
      return { ...state, count: 0 };
    default:
      return state;
  }
};

const [state, dispatch] = useReducer(reducer, { count: 0 });

Common Patterns

Fetch Data Pattern

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

  useEffect(() => {
    const controller = new AbortController();
    
    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url, { signal: controller.signal });
        const json = await response.json();
        setData(json);
        setError(null);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchData();

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

  return { data, loading, error };
}

Toggle Pattern

function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  
  return { value, toggle, setTrue, setFalse };
}

Performance Tips

  1. Avoid creating objects in render - They create new references
  2. Use React.memo wisely - Only when re-renders are expensive
  3. Virtualize long lists - Use react-window or react-virtualized
  4. Code split - Use React.lazy and Suspense
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <HeavyComponent />
    </Suspense>
  );
}

Conclusion

Following these best practices will help you write React Hooks that are clean, efficient, and maintainable. Remember to always clean up effects, include all dependencies, and extract reusable logic into custom hooks.

Share:

💬 Comments