React Hooks have revolutionized how we build React components, making it possible to use state and other React features without writing a class. In this article, we'll move beyond the basics and explore advanced hook patterns and custom hooks that can solve real-world problems.

Advanced useEffect Patterns

The useEffect hook is one of the most powerful but also most misunderstood hooks. Here are some advanced patterns:

Cleaning Up Resources

useEffect(() => {
  const subscription = subscribeToData(dataId);
  
  // Return a cleanup function
  return () => {
    subscription.unsubscribe();
  };
}, [dataId]);

Debouncing in useEffect

useEffect(() => {
  const handler = setTimeout(() => {
    // Perform an action after the user stops typing
    fetchResults(searchTerm);
  }, 500);
  
  return () => {
    clearTimeout(handler);
  };
}, [searchTerm]);

Custom Hooks for Common Patterns

Custom hooks allow you to extract component logic into reusable functions. Here are some useful custom hooks:

useLocalStorage

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

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

  return [storedValue, setValue];
}

useFetch

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

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`Error: ${response.status}`);
        }
        const json = await response.json();
        setData(json);
        setLoading(false);
      } catch (error) {
        setError(error.message);
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

The useReducer Hook for Complex State Logic

When component state becomes complex, useReducer can provide a more structured way to manage it:

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
      );
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);

  const addTodo = text => {
    dispatch({ type: 'ADD_TODO', payload: text });
  };

  const toggleTodo = id => {
    dispatch({ type: 'TOGGLE_TODO', payload: id });
  };

  const deleteTodo = id => {
    dispatch({ type: 'DELETE_TODO', payload: id });
  };

  // Rest of component...
}

Conclusion

React Hooks offer a powerful way to organize and reuse stateful logic in your components. By mastering advanced patterns and creating custom hooks, you can write more maintainable, concise, and powerful React applications. Keep experimenting with different hook patterns to find what works best for your specific use cases.