Javascript

React Hooks em profundidade Já leu

16 min de leitura

React Hooks em profundidade
No artigo anterior você aprendeu useState e useEffect — os dois hooks mais usados. Mas o React tem um ecossistema rico de hooks que resolvem problemas específicos de performance, compartilhamento de estado e lógica reuti

No artigo anterior você aprendeu useState e useEffect — os dois hooks mais usados. Mas o React tem um ecossistema rico de hooks que resolvem problemas específicos de performance, compartilhamento de estado e lógica reutilizável.

Neste artigo vamos explorar useContext, useReducer, useMemo, useCallback, useRef, useId e — o mais poderoso de todos — como criar seus próprios hooks customizados.


Regras dos Hooks

Antes de começar, as regras que nunca podem ser quebradas:

// ✅ Correto — hooks no nível do componente
function MeuComponente() {
  const [estado, setEstado] = useState(0);
  useEffect(() => {}, []);
  return <div>{estado}</div>;
}

// ❌ Nunca dentro de condicionais
function Errado() {
  if (condicao) {
    const [estado, setEstado] = useState(0); // ❌
  }
}

// ❌ Nunca dentro de loops
function Errado() {
  for (let i = 0; i < 3; i++) {
    const [estado, setEstado] = useState(0); // ❌
  }
}

// ❌ Nunca em funções comuns — apenas em componentes e hooks customizados
function minhaFuncao() {
  const [estado, setEstado] = useState(0); // ❌
}

// A razão: React depende da ordem de chamada dos hooks
// para associar estado ao componente correto

useRef — referências sem re-renderização

useRef cria um objeto mutável que persiste entre renderizações sem causar re-renderização:

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

// ── Caso 1: Referência ao elemento DOM ─────────────
function CampoAutoFoco() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current?.focus(); // acessa o elemento DOM diretamente
  }, []);

  return <input ref={inputRef} placeholder="Foca automaticamente" />;
}

// ── Caso 2: Guardar valor sem re-renderizar ─────────
function Cronometro() {
  const [tempo, setTempo] = useState(0);
  const [rodando, setRodando] = useState(false);
  const intervalRef = useRef(null);

  function iniciar() {
    if (rodando) return;
    setRodando(true);
    intervalRef.current = setInterval(() => {
      setTempo(t => t + 1);
    }, 1000);
  }

  function pausar() {
    clearInterval(intervalRef.current);
    setRodando(false);
  }

  function resetar() {
    clearInterval(intervalRef.current);
    setRodando(false);
    setTempo(0);
  }

  // Limpa o intervalo quando o componente desmonta
  useEffect(() => () => clearInterval(intervalRef.current), []);

  return (
    <div>
      <p>{String(Math.floor(tempo / 60)).padStart(2, '0')}:{String(tempo % 60).padStart(2, '0')}</p>
      <button onClick={iniciar} disabled={rodando}>▶ Iniciar</button>
      <button onClick={pausar} disabled={!rodando}>⏸ Pausar</button>
      <button onClick={resetar}>↺ Resetar</button>
    </div>
  );
}

// ── Caso 3: Valor anterior ─────────────────────────
function usePrevious(valor) {
  const ref = useRef(undefined);
  useEffect(() => { ref.current = valor; });
  return ref.current; // retorna o valor da renderização anterior
}

function Contador() {
  const [count, setCount] = useState(0);
  const anterior = usePrevious(count);

  return (
    <div>
      <p>Atual: {count} | Anterior: {anterior ?? 'nenhum'}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
    </div>
  );
}

useContext — estado global sem prop drilling

O problema que o Context resolve:

// ── Prop drilling — o problema ─────────────────────
// App passa tema para Layout que passa para Sidebar
// que passa para Menu que passa para MenuItem
// só para que MenuItem use o tema. Insuportável.

function App() {
  const [tema, setTema] = useState('claro');
  return <Layout tema={tema} setTema={setTema} />;
}
function Layout({ tema, setTema }) {
  return <Sidebar tema={tema} setTema={setTema} />;
}
function Sidebar({ tema, setTema }) {
  return <Menu tema={tema} setTema={setTema} />;
}
// ... cinco níveis depois ...
function MenuItem({ tema }) {
  return <div className={`item tema-${tema}`}>...</div>;
}
// ── Context — a solução ────────────────────────────
import { createContext, useContext, useState } from 'react';

// 1. Criar o contexto com valor padrão
const TemaContext = createContext({
  tema: 'claro',
  alternarTema: () => {},
});

// 2. Criar o Provider — envolve os componentes que precisam do valor
function TemaProvider({ children }) {
  const [tema, setTema] = useState('claro');

  function alternarTema() {
    setTema(t => t === 'claro' ? 'escuro' : 'claro');
  }

  return (
    <TemaContext.Provider value={{ tema, alternarTema }}>
      {children}
    </TemaContext.Provider>
  );
}

// Hook customizado para facilitar o uso
function useTema() {
  const contexto = useContext(TemaContext);
  if (!contexto) throw new Error('useTema deve ser usado dentro de TemaProvider');
  return contexto;
}

// 3. Usar em qualquer componente dentro do Provider — sem props
function BotaoTema() {
  const { tema, alternarTema } = useTema();
  return (
    <button onClick={alternarTema}>
      {tema === 'claro' ? '🌙 Modo escuro' : '☀️ Modo claro'}
    </button>
  );
}

function MenuItem() {
  const { tema } = useTema();
  return <div className={`item tema-${tema}`}>...</div>;
}

// 4. Aplicar o Provider no topo
function App() {
  return (
    <TemaProvider>
      <Layout />  {/* BotaoTema e MenuItem acessam o contexto diretamente */}
    </TemaProvider>
  );
}

useReducer — estado complexo com lógica centralizada

Quando o estado tem muitas transições inter-relacionadas, useReducer é mais adequado que múltiplos useState:

import { useReducer } from 'react';

// Estado inicial
const estadoInicial = {
  tarefas: [],
  filtro: 'todas',
  carregando: false,
  erro: null,
};

// Reducer — função pura: (estado, ação) → novo estado
function tarefasReducer(estado, acao) {
  switch (acao.type) {

    case 'ADICIONAR':
      return {
        ...estado,
        tarefas: [
          { id: crypto.randomUUID(), texto: acao.payload, feita: false },
          ...estado.tarefas,
        ],
      };

    case 'ALTERNAR':
      return {
        ...estado,
        tarefas: estado.tarefas.map(t =>
          t.id === acao.payload ? { ...t, feita: !t.feita } : t
        ),
      };

    case 'REMOVER':
      return {
        ...estado,
        tarefas: estado.tarefas.filter(t => t.id !== acao.payload),
      };

    case 'DEFINIR_FILTRO':
      return { ...estado, filtro: acao.payload };

    case 'LIMPAR_CONCLUIDAS':
      return { ...estado, tarefas: estado.tarefas.filter(t => !t.feita) };

    case 'CARREGAR_INICIO':
      return { ...estado, carregando: true, erro: null };

    case 'CARREGAR_SUCESSO':
      return { ...estado, carregando: false, tarefas: acao.payload };

    case 'CARREGAR_ERRO':
      return { ...estado, carregando: false, erro: acao.payload };

    default:
      return estado;
  }
}

// Componente usando useReducer
function AppTarefas() {
  const [estado, dispatch] = useReducer(tarefasReducer, estadoInicial);

  // Ações tipadas — evita erros de digitação
  const acoes = {
    adicionar: (texto) => dispatch({ type: 'ADICIONAR', payload: texto }),
    alternar: (id) => dispatch({ type: 'ALTERNAR', payload: id }),
    remover: (id) => dispatch({ type: 'REMOVER', payload: id }),
    filtrar: (filtro) => dispatch({ type: 'DEFINIR_FILTRO', payload: filtro }),
    limparConcluidas: () => dispatch({ type: 'LIMPAR_CONCLUIDAS' }),
  };

  const tarefasFiltradas = estado.tarefas.filter(t => {
    if (estado.filtro === 'pendentes') return !t.feita;
    if (estado.filtro === 'concluidas') return t.feita;
    return true;
  });

  return (
    <div>
      {estado.erro && <p className="erro">{estado.erro}</p>}
      {estado.carregando && <p>Carregando...</p>}

      <ul>
        {tarefasFiltradas.map(t => (
          <li key={t.id}>
            <input
              type="checkbox"
              checked={t.feita}
              onChange={() => acoes.alternar(t.id)}
            />
            {t.texto}
            <button onClick={() => acoes.remover(t.id)}>🗑</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

useMemo — memorizando valores calculados

import { useState, useMemo } from 'react';

function ListaFiltrada({ produtos }) {
  const [busca, setBusca] = useState('');
  const [ordenar, setOrdenar] = useState('nome');

  // SEM useMemo — filtra e ordena em TODA renderização
  // (mesmo quando só a UI mudou, não os produtos ou busca)
  const produtosProcessados = produtos
    .filter(p => p.nome.toLowerCase().includes(busca.toLowerCase()))
    .sort((a, b) => a[ordenar] > b[ordenar] ? 1 : -1);

  // COM useMemo — só recalcula quando produtos, busca ou ordenar mudam
  const produtosMemo = useMemo(() => {
    console.log('Recalculando lista...');
    return produtos
      .filter(p => p.nome.toLowerCase().includes(busca.toLowerCase()))
      .sort((a, b) => a[ordenar] > b[ordenar] ? 1 : -1);
  }, [produtos, busca, ordenar]); // dependências

  return (
    <div>
      <input value={busca} onChange={e => setBusca(e.target.value)} />
      <select value={ordenar} onChange={e => setOrdenar(e.target.value)}>
        <option value="nome">Nome</option>
        <option value="preco">Preço</option>
      </select>
      <ul>
        {produtosMemo.map(p => <li key={p.id}>{p.nome} — R$ {p.preco}</li>)}
      </ul>
    </div>
  );
}

// useMemo para objetos passados como props (evita re-renders desnecessários)
function Configuracao({ usuario }) {
  const config = useMemo(() => ({
    tema: usuario.preferencias.tema,
    idioma: usuario.preferencias.idioma,
    formato: usuario.preferencias.formatoData,
  }), [usuario.preferencias]);

  return <Painel config={config} />;
}

// ⚠️ Não use useMemo para tudo — tem custo de memória
// Use apenas quando:
// 1. O cálculo é realmente custoso (lista grande, agregação complexa)
// 2. O valor é passado para componentes filhos memorizados

useCallback — memorizando funções

import { useState, useCallback, memo } from 'react';

// React.memo — componente que só re-renderiza se as props mudarem
const BotaoItem = memo(function BotaoItem({ id, onRemover }) {
  console.log(`BotaoItem ${id} renderizou`);
  return <button onClick={() => onRemover(id)}>Remover {id}</button>;
});

function ListaItens() {
  const [itens, setItens] = useState([1, 2, 3, 4, 5]);
  const [contador, setContador] = useState(0);

  // SEM useCallback — nova função a cada renderização
  // → BotaoItem re-renderiza mesmo sem mudança nos itens!
  const removerSemMemo = (id) => setItens(prev => prev.filter(i => i !== id));

  // COM useCallback — mesma referência de função entre renderizações
  // → BotaoItem só re-renderiza quando itens muda
  const removerComMemo = useCallback((id) => {
    setItens(prev => prev.filter(i => i !== id));
  }, []); // [] porque usa o padrão funcional do setState

  return (
    <div>
      <button onClick={() => setContador(c => c + 1)}>
        Contador: {contador}  {/* clique aqui não deve re-renderizar BotaoItem */}
      </button>
      {itens.map(id => (
        <BotaoItem key={id} id={id} onRemover={removerComMemo} />
      ))}
    </div>
  );
}

// useCallback para funções passadas como dependência de useEffect
function BuscaAoVivo({ termo }) {
  const [resultados, setResultados] = useState([]);

  const buscar = useCallback(async () => {
    if (!termo) return;
    const res = await fetch(`/api/busca?q=${termo}`);
    setResultados(await res.json());
  }, [termo]); // nova função apenas quando termo muda

  useEffect(() => {
    buscar();
  }, [buscar]); // ESLint fica feliz — dependência explícita

  return <ul>{resultados.map(r => <li key={r.id}>{r.nome}</li>)}</ul>;
}

useId — IDs únicos acessíveis

import { useId } from 'react';

// Gera IDs únicos consistentes entre servidor e cliente (SSR-safe)
function CampoFormulario({ label, tipo = 'text', ...props }) {
  const id = useId(); // ex: ":r0:", ":r1:", ":r2:"...

  return (
    <div className="campo">
      <label htmlFor={id}>{label}</label>
      <input id={id} type={tipo} {...props} />
    </div>
  );
}

// Múltiplos IDs em um componente
function RangeComLabel({ label, min, max, value, onChange }) {
  const baseId = useId();
  const inputId = `${baseId}-input`;
  const listaId = `${baseId}-lista`;

  return (
    <div>
      <label htmlFor={inputId}>{label}: {value}</label>
      <input
        id={inputId}
        type="range"
        min={min}
        max={max}
        value={value}
        onChange={onChange}
        aria-describedby={listaId}
      />
    </div>
  );
}

Hooks customizados — reutilizando lógica

Hooks customizados são funções que começam com use e podem chamar outros hooks. São a forma mais poderosa de reutilização de lógica no React:

// ── useFetch — busca de dados reutilizável ─────────
function useFetch(url) {
  const [dados, setDados] = useState(null);
  const [carregando, setCarregando] = useState(true);
  const [erro, setErro] = useState(null);

  useEffect(() => {
    let cancelado = false;
    const controller = new AbortController();

    async function buscar() {
      setCarregando(true);
      setErro(null);

      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`Erro ${res.status}`);
        const json = await res.json();
        if (!cancelado) setDados(json);
      } catch (e) {
        if (!cancelado && e.name !== 'AbortError') {
          setErro(e.message);
        }
      } finally {
        if (!cancelado) setCarregando(false);
      }
    }

    buscar();

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

  return { dados, carregando, erro };
}

// Uso
function PerfilUsuario({ id }) {
  const { dados, carregando, erro } = useFetch(
    `https://jsonplaceholder.typicode.com/users/${id}`
  );

  if (carregando) return <p>Carregando...</p>;
  if (erro) return <p>Erro: {erro}</p>;
  return <div><h2>{dados?.name}</h2><p>{dados?.email}</p></div>;
}
// ── useLocalStorage — persistência automática ──────
function useLocalStorage(chave, valorInicial) {
  const [valor, setValor] = useState(() => {
    try {
      const item = localStorage.getItem(chave);
      return item !== null ? JSON.parse(item) : valorInicial;
    } catch {
      return valorInicial;
    }
  });

  const definir = useCallback((novoValor) => {
    try {
      // Permite passar função (igual ao setState)
      const valorParaSalvar =
        typeof novoValor === 'function' ? novoValor(valor) : novoValor;
      setValor(valorParaSalvar);
      localStorage.setItem(chave, JSON.stringify(valorParaSalvar));
    } catch (e) {
      console.warn(`useLocalStorage: erro ao salvar "${chave}"`, e);
    }
  }, [chave, valor]);

  return [valor, definir];
}

// Uso — idêntico ao useState, mas persiste!
function Preferencias() {
  const [tema, setTema] = useLocalStorage('tema', 'claro');
  const [idioma, setIdioma] = useLocalStorage('idioma', 'pt-BR');

  return (
    <div>
      <select value={tema} onChange={e => setTema(e.target.value)}>
        <option value="claro">☀️ Claro</option>
        <option value="escuro">🌙 Escuro</option>
      </select>
    </div>
  );
}
// ── useDebounce — atraso de valor ──────────────────
function useDebounce(valor, delay = 500) {
  const [valorAtrasado, setValorAtrasado] = useState(valor);

  useEffect(() => {
    const timer = setTimeout(() => setValorAtrasado(valor), delay);
    return () => clearTimeout(timer);
  }, [valor, delay]);

  return valorAtrasado;
}

// ── useWindowSize — dimensões da janela ────────────
function useWindowSize() {
  const [tamanho, setTamanho] = useState({
    largura: window.innerWidth,
    altura: window.innerHeight,
  });

  useEffect(() => {
    function handleResize() {
      setTamanho({ largura: window.innerWidth, altura: window.innerHeight });
    }

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

  return tamanho;
}

// ── useOnClickOutside — fechar ao clicar fora ──────
function useOnClickOutside(ref, handler) {
  useEffect(() => {
    function listener(evento) {
      if (!ref.current || ref.current.contains(evento.target)) return;
      handler(evento);
    }

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

// ── useMediaQuery — responsive no código ───────────
function useMediaQuery(query) {
  const [corresponde, setCorresponde] = useState(
    () => window.matchMedia(query).matches
  );

  useEffect(() => {
    const media = window.matchMedia(query);
    function handleChange(e) { setCorresponde(e.matches); }
    media.addEventListener('change', handleChange);
    return () => media.removeEventListener('change', handleChange);
  }, [query]);

  return corresponde;
}

// Uso
function Layout() {
  const eMobile = useMediaQuery('(max-width: 768px)');
  const prefereEscuro = useMediaQuery('(prefers-color-scheme: dark)');

  return (
    <div>
      {eMobile ? <MenuMobile /> : <MenuDesktop />}
      {prefereEscuro && <p>Você prefere o modo escuro!</p>}
    </div>
  );
}
// ── useForm — gerenciamento completo de formulário ──
function useForm(camposIniciais, validar) {
  const [valores, setValores] = useState(camposIniciais);
  const [erros, setErros] = useState({});
  const [tocados, setTocados] = useState({});
  const [enviando, setEnviando] = useState(false);

  function handleChange(e) {
    const { name, value, type, checked } = e.target;
    const novoValor = type === 'checkbox' ? checked : value;

    setValores(prev => ({ ...prev, [name]: novoValor }));

    // Valida campo em tempo real se já foi tocado
    if (tocados[name] && validar) {
      const novosErros = validar({ ...valores, [name]: novoValor });
      setErros(prev => ({ ...prev, [name]: novosErros[name] }));
    }
  }

  function handleBlur(e) {
    const { name } = e.target;
    setTocados(prev => ({ ...prev, [name]: true }));

    if (validar) {
      const novosErros = validar(valores);
      setErros(prev => ({ ...prev, [name]: novosErros[name] }));
    }
  }

  async function handleSubmit(onSubmit) {
    return async (e) => {
      e.preventDefault();

      // Marca todos como tocados e valida
      const todosTocados = Object.keys(camposIniciais).reduce(
        (acc, k) => ({ ...acc, [k]: true }), {}
      );
      setTocados(todosTocados);

      if (validar) {
        const novosErros = validar(valores);
        setErros(novosErros);
        if (Object.values(novosErros).some(Boolean)) return;
      }

      setEnviando(true);
      try {
        await onSubmit(valores);
      } finally {
        setEnviando(false);
      }
    };
  }

  function resetar() {
    setValores(camposIniciais);
    setErros({});
    setTocados({});
  }

  return {
    valores,
    erros,
    tocados,
    enviando,
    handleChange,
    handleBlur,
    handleSubmit,
    resetar,
  };
}

// Uso
function FormularioCadastro() {
  const { valores, erros, tocados, enviando, handleChange, handleBlur, handleSubmit } =
    useForm(
      { nome: '', email: '', senha: '' },
      (v) => ({
        nome: !v.nome ? 'Nome é obrigatório.' : '',
        email: !v.email.includes('@') ? 'Email inválido.' : '',
        senha: v.senha.length < 6 ? 'Mínimo 6 caracteres.' : '',
      })
    );

  const aoEnviar = handleSubmit(async (dados) => {
    await fetch('/api/usuarios', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(dados),
    });
  });

  return (
    <form onSubmit={aoEnviar}>
      <input
        name="nome"
        value={valores.nome}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      {tocados.nome && erros.nome && <span className="erro">{erros.nome}</span>}

      <input
        name="email"
        type="email"
        value={valores.email}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      {tocados.email && erros.email && <span className="erro">{erros.email}</span>}

      <input
        name="senha"
        type="password"
        value={valores.senha}
        onChange={handleChange}
        onBlur={handleBlur}
      />
      {tocados.senha && erros.senha && <span className="erro">{erros.senha}</span>}

      <button type="submit" disabled={enviando}>
        {enviando ? 'Enviando...' : 'Cadastrar'}
      </button>
    </form>
  );
}

Quando usar cada hook

┌──────────────────────────────────────────────────────────┐
│                  GUIA DE DECISÃO                         │
│                                                          │
│  useState      → estado simples, independente            │
│  useReducer    → estado complexo com múltiplas ações     │
│  useContext    → estado global / compartilhado           │
│  useRef        → DOM direto / valor sem re-render        │
│  useEffect     → efeitos colaterais / fetch / timers     │
│  useMemo       → cálculo custoso memorizado              │
│  useCallback   → função memorizada (para memo/Effect)    │
│  useId         → IDs únicos para acessibilidade          │
│  Hook custom   → lógica reutilizável entre componentes   │
│                                                          │
│  ⚠️ Não otimize prematuramente:                          │
│  useMemo e useCallback têm custo.                        │
│  Só use quando há problema de performance real.          │
└──────────────────────────────────────────────────────────┘

Tarefa para você

Construa uma biblioteca de hooks customizados para o projeto:

// 1. useContador(inicial, { min, max, passo })
//    - increment, decrement, resetar
//    - respeita min e max
//    - passo configurável

// 2. useToggle(valorInicial)
//    - toggle, ativar, desativar
//    - retorna [valor, { toggle, ativar, desativar }]

// 3. useAsync(funcaoAssincrona, dependencias)
//    - executa a função automaticamente
//    - retorna { dados, carregando, erro, executar }
//    - executar() permite refazer manualmente

// 4. useCopiarTexto()
//    - retorna [copiado, copiar]
//    - copiar(texto) copia para clipboard
//    - copiado fica true por 2 segundos depois volta false

// 5. useHistorico(valorInicial)
//    - rastreia histórico de mudanças
//    - retorna [atual, { definir, desfazer, refazer, limpar }]
//    - desfazer e refazer navegam pelo histórico

// Para cada hook: escreva testes com Jest
// (simule o hook com @testing-library/react-hooks
//  ou renderize um componente de teste)

Conclusão

Neste artigo você aprendeu:

  • As regras imutáveis dos hooks e por que existem
  • useRef — referências ao DOM e valores persistentes sem re-renderização
  • useContext — eliminando prop drilling com estado global
  • useReducer — gerenciamento de estado complexo com reducer puro
  • useMemo — memorizando valores calculados para performance
  • useCallback — memorizando funções para evitar re-renders
  • useId — gerando IDs únicos acessíveis e SSR-safe
  • Como criar hooks customizados — a forma mais poderosa de reutilização
  • Hooks prontos para uso: useFetch, useLocalStorage, useDebounce, useWindowSize, useOnClickOutside, useMediaQuery, useForm
  • O guia de decisão — quando usar cada hook

No próximo artigo vamos aprender gerenciamento de estado com Zustand e como conectar nossa API do back-end ao front-end React com React Query.


📌 Próximo artigo: Aula 40 — Estado global com Zustand e React Query


📚 Fontes e Referências

  • React — Hooks Reference: https://react.dev/reference/react
  • React — useContext: https://react.dev/reference/react/useContext
  • React — useReducer: https://react.dev/reference/react/useReducer
  • React — useMemo: https://react.dev/reference/react/useMemo
  • React — useCallback: https://react.dev/reference/react/useCallback
  • React — Custom Hooks: https://react.dev/learn/reusing-logic-with-custom-hooks
  • usehooks.com — Coleção de hooks customizados: https://usehooks.com
  • React Hooks in Action — John Larsen (Manning)
  • Learning React — Alex Banks e Eve Porcello (O'Reilly)
  • roadmap.sh — React: https://roadmap.sh/react
Comentários

Mais em Javascript

Revisão + Projeto Final: SPA Completa
Revisão + Projeto Final: SPA Completa

Chegamos ao fim de mais uma etapa. Em cinco artigos você aprendeu React do ze...

Tratamento de erros em requisições HTTP
Tratamento de erros em requisições HTTP

Fazer uma requisição funcionar no ambiente de desenvolvimento é fácil. O verd...

O que é JavaScript e por que aprender?
O que é JavaScript e por que aprender?

Se voc&ecirc; quer trabalhar com desenvolvimento web &mdash; seja criando sit...