---
source_hash: "f271b48e"
title: "Chapter 2: Custom Hooks"
weight: 460
---

# Chapter 2: Custom Hooks

## What is a Custom Hook?

A custom hook is a JavaScript function that:

1. Starts with `use` (mandatory convention)
2. Can call other hooks
3. Encapsulates reusable logic

```javascript
// Custom hook
function useCounter(initialValue = 0) {
    const [count, setCount] = useState(initialValue);

    const increment = () => setCount(c => c + 1);
    const decrement = () => setCount(c => c - 1);
    const reset = () => setCount(initialValue);

    return { count, increment, decrement, reset };
}

// Usage
function Counter() {
    const { count, increment, decrement, reset } = useCounter(10);

    return (
        <div>
            <p>{count}</p>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
            <button onClick={reset}>Reset</button>
        </div>
    );
}
```

## Why Create Custom Hooks?

1. **Reusability**: same logic in multiple components
2. **Separation**: isolate logic from display
3. **Tests**: easier to test than components
4. **Readability**: simpler components

## Example: useLocalStorage

Hook that synchronizes state with localStorage:

```javascript
function useLocalStorage(key, initialValue) {
    // Initialize from localStorage
    const [value, setValue] = useState(() => {
        try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            console.error(error);
            return initialValue;
        }
    });

    // Update localStorage when value changes
    useEffect(() => {
        try {
            window.localStorage.setItem(key, JSON.stringify(value));
        } catch (error) {
            console.error(error);
        }
    }, [key, value]);

    return [value, setValue];
}

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

    return (
        <div>
            <select value={theme} onChange={e => setTheme(e.target.value)}>
                <option value="light">Light</option>
                <option value="dark">Dark</option>
            </select>
            <select value={language} onChange={e => setLanguage(e.target.value)}>
                <option value="fr">French</option>
                <option value="en">English</option>
            </select>
        </div>
    );
}
```

## Example: useFetch

Hook for API calls:

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

    useEffect(() => {
        let cancelled = false;

        const fetchData = async () => {
            setLoading(true);
            setError(null);

            try {
                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}`);
                }
                const json = await response.json();

                if (!cancelled) {
                    setData(json);
                }
            } catch (err) {
                if (!cancelled) {
                    setError(err.message);
                }
            } finally {
                if (!cancelled) {
                    setLoading(false);
                }
            }
        };

        fetchData();

        return () => {
            cancelled = true;
        };
    }, [url]);

    return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
    const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

    if (loading) return <p>Loading...</p>;
    if (error) return <p>Error: {error}</p>;

    return <div>{user.name}</div>;
}
```

## Example: useDebounce

Hook to debounce a value (useful for search):

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

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

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

    return debouncedValue;
}

// Usage
function SearchInput({ onSearch }) {
    const [query, setQuery] = useState('');
    const debouncedQuery = useDebounce(query, 300);

    useEffect(() => {
        if (debouncedQuery) {
            onSearch(debouncedQuery);
        }
    }, [debouncedQuery, onSearch]);

    return (
        <input
            value={query}
            onChange={e => setQuery(e.target.value)}
            placeholder="Search..."
        />
    );
}
```

## Example: useToggle

Simple hook for boolean values:

```javascript
function useToggle(initialValue = false) {
    const [value, setValue] = useState(initialValue);

    const toggle = useCallback(() => {
        setValue(v => !v);
    }, []);

    const setTrue = useCallback(() => setValue(true), []);
    const setFalse = useCallback(() => setValue(false), []);

    return { value, toggle, setTrue, setFalse };
}

// Usage
function Modal() {
    const { value: isOpen, toggle, setFalse: close } = useToggle();

    return (
        <div>
            <button onClick={toggle}>Open/Close</button>
            {isOpen && (
                <div className="modal">
                    <p>Modal content</p>
                    <button onClick={close}>Close</button>
                </div>
            )}
        </div>
    );
}
```

## Example: useForm

Hook for managing forms:

```javascript
function useForm(initialValues) {
    const [values, setValues] = useState(initialValues);
    const [errors, setErrors] = useState({});

    const handleChange = useCallback((e) => {
        const { name, value, type, checked } = e.target;
        setValues(prev => ({
            ...prev,
            [name]: type === 'checkbox' ? checked : value
        }));
    }, []);

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

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

    const validate = useCallback((validationRules) => {
        const newErrors = {};
        for (const [field, rules] of Object.entries(validationRules)) {
            for (const rule of rules) {
                const error = rule(values[field], values);
                if (error) {
                    newErrors[field] = error;
                    break;
                }
            }
        }
        setErrors(newErrors);
        return Object.keys(newErrors).length === 0;
    }, [values]);

    return {
        values,
        errors,
        handleChange,
        setValue,
        reset,
        validate
    };
}

// Usage
function LoginForm({ onSubmit }) {
    const { values, errors, handleChange, validate } = useForm({
        email: '',
        password: ''
    });

    const validationRules = {
        email: [
            v => !v && 'Email required',
            v => !v.includes('@') && 'Invalid email'
        ],
        password: [
            v => !v && 'Password required',
            v => v.length < 6 && 'Minimum 6 characters'
        ]
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        if (validate(validationRules)) {
            onSubmit(values);
        }
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <input
                    name="email"
                    value={values.email}
                    onChange={handleChange}
                    placeholder="Email"
                />
                {errors.email && <span className="error">{errors.email}</span>}
            </div>
            <div>
                <input
                    name="password"
                    type="password"
                    value={values.password}
                    onChange={handleChange}
                    placeholder="Password"
                />
                {errors.password && <span className="error">{errors.password}</span>}
            </div>
            <button type="submit">Login</button>
        </form>
    );
}
```

## Example: useOnClickOutside

Hook to detect clicks outside an element:

```javascript
function useOnClickOutside(ref, handler) {
    useEffect(() => {
        const listener = (event) => {
            if (!ref.current || ref.current.contains(event.target)) {
                return;
            }
            handler(event);
        };

        document.addEventListener('mousedown', listener);
        document.addEventListener('touchstart', listener);

        return () => {
            document.removeEventListener('mousedown', listener);
            document.removeEventListener('touchstart', listener);
        };
    }, [ref, handler]);
}

// Usage
function Dropdown() {
    const [isOpen, setIsOpen] = useState(false);
    const dropdownRef = useRef(null);

    useOnClickOutside(dropdownRef, () => setIsOpen(false));

    return (
        <div ref={dropdownRef}>
            <button onClick={() => setIsOpen(!isOpen)}>Menu</button>
            {isOpen && (
                <ul className="dropdown-menu">
                    <li>Option 1</li>
                    <li>Option 2</li>
                    <li>Option 3</li>
                </ul>
            )}
        </div>
    );
}
```

## Custom Hooks Rules

1. **Name starts with `use`**: required for React to apply hook rules
2. **Can call other hooks**: useState, useEffect, useCallback, other custom hooks
3. **Returns what the component needs**: values, functions, objects
4. **Each call is independent**: two components using the same hook have separate states

## File Organization

```
src/
  hooks/
    useLocalStorage.js
    useFetch.js
    useDebounce.js
    useToggle.js
    useForm.js
    index.js  // export all hooks
```

```javascript
// hooks/index.js
export { useLocalStorage } from './useLocalStorage';
export { useFetch } from './useFetch';
export { useDebounce } from './useDebounce';
export { useToggle } from './useToggle';
export { useForm } from './useForm';
```

## Exercises

### Exercise 1: useWindowSize

Create a hook that returns window dimensions and updates on resize.

**Solution:**

```javascript
function useWindowSize() {
    const [size, setSize] = useState({
        width: window.innerWidth,
        height: window.innerHeight
    });

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

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

    return size;
}

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

    return (
        <div>
            Window: {width} x {height}
            {width < 768 && <p>Mobile mode</p>}
        </div>
    );
}
```

### Exercise 2: usePrevious

Create a hook that returns the previous value of a variable.

**Solution:**

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

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

    return ref.current;
}

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

    return (
        <div>
            <p>Current: {count}</p>
            <p>Previous: {previousCount}</p>
            <button onClick={() => setCount(c => c + 1)}>+1</button>
        </div>
    );
}
```

## Key Points to Remember

1. **Custom hook = function starting with `use`**
2. **Encapsulates reusable logic** between components
3. **Each call has its own state** independent
4. **Can call other hooks** (useState, useEffect, etc.)
5. **Simplifies components** by extracting logic

[<- Previous Chapter](/training/module4-react-avance/usecallback-usememo) | [Back to module](/training/module4-react-avance) | [Next Chapter: Redux ->](/training/module4-react-avance/redux)
