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

Mini Projeto: App de Clima Completo
Mini Projeto: App de Clima Completo

Chegamos ao fim do Módulo 3. Em oito artigos você percorreu um caminho que mu...

NPM: gerenciando pacotes e dependências
NPM: gerenciando pacotes e dependências

Uma das maiores vantagens do Node.js não é o que ele faz sozinho — é o que a...

Fetch API: consumindo dados da internet
Fetch API: consumindo dados da internet

At&eacute; agora simulamos requisi&ccedil;&otilde;es com&nbsp;setTimeout. A p...