Frontend Development

React Hooks Deep Dive: useEffect and Custom Hooks

Master React Hooks with advanced patterns, custom hook creation, and best practices for managing side effects in modern React applications.

Sasank - BTech CSE Student
February 20, 2025
11 min read
React Hooks Deep Dive: useEffect and Custom Hooks
React
Hooks
JavaScript
Frontend

React Hooks Deep Dive: useEffect and Custom Hooks

React Hooks revolutionized how we write React components by allowing us to use state and other React features in functional components. In this comprehensive guide, we'll explore useEffect and custom hooks with advanced patterns and best practices.

Understanding useEffect

The useEffect hook lets you perform side effects in functional components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined in React class components.

Basic useEffect Usage

import React, { useState, useEffect } from 'react';

function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(/api/users/${userId});
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
}

fetchUser();
}, [userId]); // Dependency array

if (loading) return
Loading...
;
if (!user) return
User not found
;

return (

{user.name}


{user.email}



);
}


## useEffect Patterns

### 1. Effect with Cleanup

function Timer() {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);

// Cleanup function
return () => {
clearInterval(interval);
};
}, []); // Empty dependency array - runs once

return
Timer: {seconds}s
;
}


### 2. Effect with Dependencies

function SearchResults({ query, filters }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);

useEffect(() => {
if (!query) {
setResults([]);
return;
}

setLoading(true);

const searchParams = new URLSearchParams({
q: query,
...filters
});

fetch(/api/search?${searchParams})
.then(response => response.json())
.then(data => {
setResults(data.results);
setLoading(false);
})
.catch(error => {
console.error('Search failed:', error);
setLoading(false);
});
}, [query, filters]); // Re-run when query or filters change

return (

{loading &&
Searching...
}
{results.map(result => (
{result.title}

))}

);
}


### 3. Conditional Effects

function DocumentTitle({ title, shouldUpdate }) {
useEffect(() => {
if (shouldUpdate) {
document.title = title;
}
}, [title, shouldUpdate]);

return null;
}


## Advanced useEffect Patterns

### 1. Debounced Effect

function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

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

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

return debouncedValue;
}

function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);

useEffect(() => {
if (debouncedSearchTerm) {
// Perform search
console.log('Searching for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);

return (
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}


### 2. Previous Value Hook

function usePrevious(value) {
const ref = useRef();

useEffect(() => {
ref.current = value;
});

return ref.current;
}

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

return (

Current: {count}


Previous: {prevCount}




);
}


## Custom Hooks

Custom hooks are JavaScript functions that start with "use" and can call other hooks. They let you extract component logic into reusable functions.

### 1. Data Fetching Hook

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

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

async function fetchData() {
try {
setLoading(true);
setError(null);

const response = await fetch(url, {
...options,
signal: abortController.signal
});

if (!response.ok) {
throw new Error(HTTP error! status: ${response.status});
}

const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
}

fetchData();

return () => {
abortController.abort();
};
}, [url, JSON.stringify(options)]);

return { data, loading, error };
}

// Usage
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');

if (loading) return
Loading users...
;
if (error) return
Error: {error}
;

return (

    {users?.map(user => (
  • {user.name}

  • ))}

);
}


### 2. Local Storage Hook

function useLocalStorage(key, initialValue) {
// Get from local storage then parse stored json or return initialValue
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(Error reading localStorage key "${key}":, error);
return initialValue;
}
});

// Return a wrapped version of useState's setter function that persists the new value to localStorage
const setValue = useCallback((value) => {
try {
// Allow value to be a function so we have the same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(Error setting localStorage key "${key}":, error);
}
}, [key, storedValue]);

return [storedValue, setValue];
}

// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'en');

return (





);
}


### 3. Window Size Hook

function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});

useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}

window.addEventListener('resize', handleResize);
handleResize(); // Call handler right away so state gets updated with initial window size

return () => window.removeEventListener('resize', handleResize);
}, []);

return windowSize;
}

// Usage
function ResponsiveComponent() {
const { width, height } = useWindowSize();

return (

Window size: {width} x {height}


{width < 768 ? (

) : (

)}

);
}


### 4. Form Hook

function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});

const handleChange = useCallback((name, value) => {
setValues(prev => ({
...prev,
[name]: value
}));

// Clear error when user starts typing
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: undefined
}));
}
}, [errors]);

const handleBlur = useCallback((name) => {
setTouched(prev => ({
...prev,
[name]: true
}));

if (validate) {
const fieldErrors = validate({ [name]: values[name] });
setErrors(prev => ({
...prev,
...fieldErrors
}));
}
}, [values, validate]);

const handleSubmit = useCallback((onSubmit) => {
return (e) => {
e.preventDefault();

if (validate) {
const formErrors = validate(values);
setErrors(formErrors);

if (Object.keys(formErrors).length === 0) {
onSubmit(values);
}
} else {
onSubmit(values);
}
};
}, [values, validate]);

const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);

return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset
};
}

// Usage
function ContactForm() {
const validate = (values) => {
const errors = {};

if (!values.name) {
errors.name = 'Name is required';
}

if (!values.email) {
errors.email = 'Email is required';
} else if (!/S+@S+.S+/.test(values.email)) {
errors.email = 'Email is invalid';
}

return errors;
};

const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
reset
} = useForm({ name: '', email: '', message: '' }, validate);

const onSubmit = (formData) => {
console.log('Form submitted:', formData);
reset();
};

return (


type="text"
placeholder="Name"
value={values.name}
onChange={(e) => handleChange('name', e.target.value)}
onBlur={() => handleBlur('name')}
/>
{touched.name && errors.name && (
{errors.name}
)}



type="email"
placeholder="Email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
/>
{touched.email && errors.email && (
{errors.email}
)}



placeholder="Message"
value={values.message}
onChange={(e) => handleChange('message', e.target.value)}
onBlur={() => handleBlur('message')}
/>





);
}


## Best Practices

### 1. Optimize Dependencies
Always include all values from component scope that are used inside useEffect in the dependencies array.

// ❌ Bad - missing dependency
function BadExample({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Missing userId dependency

return
{user?.name}
;
}

// ✅ Good - all dependencies included
function GoodExample({ userId }) {
const [user, setUser] = useState(null);

useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // userId included

return
{user?.name}
;
}


### 2. Separate Concerns
Split unrelated logic into separate useEffect hooks.

function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);

// Separate effect for user data
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);

// Separate effect for posts data
useEffect(() => {
fetchUserPosts(userId).then(setPosts);
}, [userId]);

return (




);
}


### 3. Use Cleanup Functions
Always clean up subscriptions, timers, and other side effects.

function WebSocketComponent() {
const [messages, setMessages] = useState([]);

useEffect(() => {
const ws = new WebSocket('ws://localhost:8080');

ws.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};

// Cleanup function
return () => {
ws.close();
};
}, []);

return (

{messages.map((msg, index) => (
{msg.text}

))}

);
}


## Conclusion

React Hooks, particularly useEffect and custom hooks, provide powerful patterns for managing side effects and sharing logic between components. Key takeaways:

- **useEffect** replaces lifecycle methods in functional components
- **Dependencies array** controls when effects run
- **Cleanup functions** prevent memory leaks
- **Custom hooks** enable logic reuse across components
- **Separation of concerns** keeps code maintainable

Master these patterns to write more efficient, reusable, and maintainable React applications.