Tecnologia & Gadgets

React Hooks: Guia Prático para Desenvolvimento Funcional Moderno

React Hooks: Guia Prático para Desenvolvimento Funcional Moderno


Os React Hooks transformaram fundamentalmente a forma como desenvolvemos componentes React. Introduzidos na versão 16.8, eles permitem usar estado e outras funcionalidades do React em componentes funcionais, eliminando a necessidade de classes na maioria dos casos.

useState: Gerenciando Estado Local

O useState é o hook mais básico e fundamental para gerenciar estado em componentes funcionais:

import React, { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('');
    
    const increment = () => setCount(count + 1);
    const decrement = () => setCount(prev => prev - 1); // Forma funcional
    
    return (
        <div>
            <input 
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Digite seu nome"
            />
            <p>Olá {name}, contador: {count}</p>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
        </div>
    );
}

Padrões Avançados com useState

// Estado complexo com objeto
function UserProfile() {
    const [user, setUser] = useState({
        name: '',
        email: '',
        preferences: {
            theme: 'light',
            notifications: true
        }
    });
    
    const updateUser = (field, value) => {
        setUser(prev => ({
            ...prev,
            [field]: value
        }));
    };
    
    const updatePreference = (key, value) => {
        setUser(prev => ({
            ...prev,
            preferences: {
                ...prev.preferences,
                [key]: value
            }
        }));
    };
    
    return (
        // JSX do componente
    );
}

useEffect: Efeitos Colaterais

O useEffect substitui componentDidMount, componentDidUpdate e componentWillUnmount em uma única API:

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

function UserData({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        let cancelled = false;
        
        async function fetchUser() {
            try {
                setLoading(true);
                setError(null);
                
                const response = await fetch(`/api/users/${userId}`);
                const userData = await response.json();
                
                if (!cancelled) {
                    setUser(userData);
                }
            } catch (err) {
                if (!cancelled) {
                    setError(err.message);
                }
            } finally {
                if (!cancelled) {
                    setLoading(false);
                }
            }
        }
        
        fetchUser();
        
        // Cleanup function
        return () => {
            cancelled = true;
        };
    }, [userId]); // Dependency array
    
    if (loading) return <div>Carregando...</div>;
    if (error) return <div>Erro: {error}</div>;
    if (!user) return <div>Usuário não encontrado</div>;
    
    return <div>{user.name}</div>;
}

Padrões de useEffect

// Efeito que roda apenas uma vez (componentDidMount)
useEffect(() => {
    console.log('Componente montado');
}, []);

// Efeito que roda a cada render
useEffect(() => {
    console.log('Componente renderizado');
});

// Efeito com cleanup (componentWillUnmount)
useEffect(() => {
    const timer = setInterval(() => {
        console.log('Timer tick');
    }, 1000);
    
    return () => clearInterval(timer);
}, []);

// Efeito condicional
useEffect(() => {
    if (user && user.id) {
        trackUserActivity(user.id);
    }
}, [user]);

useContext: Compartilhando Estado

O useContext simplifica o consumo de Context API:

// Criando o contexto
const ThemeContext = React.createContext();

// Provider
function ThemeProvider({ children }) {
    const [theme, setTheme] = useState('light');
    
    const toggleTheme = () => {
        setTheme(prev => prev === 'light' ? 'dark' : 'light');
    };
    
    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}

// Consumindo o contexto
function ThemedButton() {
    const { theme, toggleTheme } = useContext(ThemeContext);
    
    return (
        <button 
            className={`btn btn-${theme}`}
            onClick={toggleTheme}
        >
            Tema atual: {theme}
        </button>
    );
}

useReducer: Estado Complexo

Para estado mais complexo, useReducer oferece mais controle:

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

// Componente usando useReducer
function TodoApp() {
    const [state, dispatch] = useReducer(todoReducer, {
        todos: [],
        filter: 'all'
    });
    
    const addTodo = (text) => {
        dispatch({ type: 'ADD_TODO', payload: text });
    };
    
    const toggleTodo = (id) => {
        dispatch({ type: 'TOGGLE_TODO', payload: id });
    };
    
    return (
        // JSX do componente
    );
}

Hooks Customizados

Criar hooks customizados permite reutilizar lógica entre componentes:

// Hook para requisições HTTP
function useApi(url) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    
    useEffect(() => {
        let cancelled = false;
        
        async function fetchData() {
            try {
                setLoading(true);
                setError(null);
                
                const response = await fetch(url);
                const result = await response.json();
                
                if (!cancelled) {
                    setData(result);
                }
            } catch (err) {
                if (!cancelled) {
                    setError(err);
                }
            } finally {
                if (!cancelled) {
                    setLoading(false);
                }
            }
        }
        
        fetchData();
        
        return () => {
            cancelled = true;
        };
    }, [url]);
    
    return { data, loading, error };
}

// Hook para localStorage
function useLocalStorage(key, initialValue) {
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            console.error('Erro ao ler localStorage:', error);
            return initialValue;
        }
    });
    
    const setValue = (value) => {
        try {
            setStoredValue(value);
            window.localStorage.setItem(key, JSON.stringify(value));
        } catch (error) {
            console.error('Erro ao salvar no localStorage:', error);
        }
    };
    
    return [storedValue, setValue];
}

// Uso dos hooks customizados
function UserProfile() {
    const { data: user, loading, error } = useApi('/api/user');
    const [preferences, setPreferences] = useLocalStorage('userPrefs', {});
    
    if (loading) return <div>Carregando...</div>;
    if (error) return <div>Erro: {error.message}</div>;
    
    return (
        <div>
            <h1>{user.name}</h1>
            <p>Tema: {preferences.theme || 'padrão'}</p>
        </div>
    );
}

useMemo e useCallback: Otimização

Para otimização de performance, use useMemo e useCallback:

import React, { useState, useMemo, useCallback } from 'react';

function ExpensiveComponent({ items, filter }) {
    // Memoiza cálculo custoso
    const filteredItems = useMemo(() => {
        console.log('Filtrando itens...');
        return items.filter(item => 
            item.name.toLowerCase().includes(filter.toLowerCase())
        );
    }, [items, filter]);
    
    // Memoiza função para evitar re-renders desnecessários
    const handleItemClick = useCallback((itemId) => {
        console.log('Item clicado:', itemId);
        // Lógica do click
    }, []); // Sem dependências = função nunca muda
    
    const handleItemUpdate = useCallback((itemId, newData) => {
        // Lógica de atualização
    }, [items]); // Recria apenas quando items mudam
    
    return (
        <div>
            {filteredItems.map(item => (
                <ItemComponent
                    key={item.id}
                    item={item}
                    onClick={handleItemClick}
                    onUpdate={handleItemUpdate}
                />
            ))}
        </div>
    );
}

useRef: Referências e Valores Mutáveis

O useRef serve para acessar elementos DOM e manter valores mutáveis:

function FocusInput() {
    const inputRef = useRef(null);
    const renderCount = useRef(0);
    
    useEffect(() => {
        renderCount.current += 1;
        console.log(`Render #${renderCount.current}`);
    });
    
    const focusInput = () => {
        inputRef.current.focus();
    };
    
    return (
        <div>
            <input ref={inputRef} type="text" />
            <button onClick={focusInput}>Focar Input</button>
            <p>Renders: {renderCount.current}</p>
        </div>
    );
}

Regras dos Hooks

Os Hooks têm regras importantes que devem ser seguidas:

1. Apenas no Nível Superior

// ❌ Não faça isso
function BadComponent({ condition }) {
    if (condition) {
        const [state, setState] = useState(0); // Erro!
    }
    
    return <div>{state}</div>;
}

// ✅ Faça isso
function GoodComponent({ condition }) {
    const [state, setState] = useState(0);
    
    if (!condition) {
        return null;
    }
    
    return <div>{state}</div>;
}

2. Apenas em Componentes React

// ❌ Não use hooks em funções regulares
function regularFunction() {
    const [state, setState] = useState(0); // Erro!
}

// ✅ Use em componentes ou hooks customizados
function MyComponent() {
    const [state, setState] = useState(0); // OK
}

function useMyHook() {
    const [state, setState] = useState(0); // OK
}

Padrões Avançados

Hook para Debounce

function useDebounce(value, delay) {
    const [debouncedValue, setDebouncedValue] = useState(value);
    
    useEffect(() => {
        const handler = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);
        
        return () => {
            clearTimeout(handler);
        };
    }, [value, delay]);
    
    return debouncedValue;
}

// Uso
function SearchComponent() {
    const [searchTerm, setSearchTerm] = useState('');
    const debouncedSearchTerm = useDebounce(searchTerm, 500);
    
    useEffect(() => {
        if (debouncedSearchTerm) {
            // Fazer busca apenas após 500ms de inatividade
            performSearch(debouncedSearchTerm);
        }
    }, [debouncedSearchTerm]);
    
    return (
        <input
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            placeholder="Buscar..."
        />
    );
}

Testing com Hooks

Para testar componentes com hooks, use React Testing Library:

import { render, screen, fireEvent } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';

// Testando componente
test('counter increments when button is clicked', () => {
    render(<Counter />);
    
    const button = screen.getByText('+');
    const counter = screen.getByText(/contador: 0/);
    
    fireEvent.click(button);
    
    expect(screen.getByText(/contador: 1/)).toBeInTheDocument();
});

// Testando hook customizado
test('useCounter hook', () => {
    const { result } = renderHook(() => useCounter(0));
    
    expect(result.current.count).toBe(0);
    
    act(() => {
        result.current.increment();
    });
    
    expect(result.current.count).toBe(1);
});

Conclusão

Os React Hooks representam uma mudança paradigmática no desenvolvimento React, oferecendo uma API mais simples e poderosa para gerenciar estado e efeitos colaterais. Eles promovem reutilização de lógica, simplificam testes e tornam os componentes mais legíveis.

A chave para dominar hooks está em entender quando usar cada um, como criar hooks customizados eficazes e seguir as regras fundamentais. Com essas práticas, você pode criar aplicações React mais maintíveis e performáticas.