Como publicar uma biblioteca de hooks React no npm usando Turborepo

Como publicar uma biblioteca de hooks React no npm usando Turborepo

Se você já trabalhou em projetos grandes, sabe o quanto é comum precisar reutilizar código.

Por isso, uma alternativa usada por grandes times é usar um monorepo.

Monorepo é um único repositório para vários projetos ou dependências que faz com que seu time tenha algumas melhorias no desenvolvimento, como: centralização do código, compartilhamento fácil de dependências e pacotes, ferramentas, não precisar ficar pulando entre repositórios e muito mais.

Quando você tem duas aplicações React diferentes no mesmo repositório, e elas reaproveitam algum tipo de lógica complexa como usar o serviço de geolocalização, adicionar scripts a página, armazenar dados no localStorage ou sessionStorage, como boa prática você abstrai essa lógica e cria hooks customizados. Certo?.

Mas para não duplicar código nas duas aplicações, o que você faz? É isto que vamos descobrir neste artigo.

Você vai aprender:

  • Criar um monorepo utilizando Turborepo.
  • Configurar e criar pacotes dentro do monorepo.
  • Consumir esses pacotes em diferentes aplicações.
  • Publicar os pacotes no npm para uso interno ou externo.

E muito mais. Então vem comigo!

Como otimizar o desenvolvimento

Imagina que você trabalha em um e-commerce. Neste serviço, o usuário precisa preencher seu endereço em algum formulário para calcular a taxa de entrega. Este endereço é preenchido uma vez se o usuário tiver cadastro, mas se não terá que preencher o CEP para calcular a taxa de entrega na tela de produto e de finalização da compra.

Então, como desenvolvedor(a) para facilitar o preenchimento dos dados de endereço do usuário, você cria um hook que a partir do CEP (código de endereçamento postal) ele retorna o endereço completo, preenchendo automaticamente os campos como bairro, município e estado, melhorando a usabilidade da aplicação e experiência do usuário.

Este hook é tão útil nestes cenários que você resolveu compartilhar ele com sua equipe de desenvolvimento.

E para não duplicar o código do hook nas diferentes aplicações desenvolvidas dentro do e-commerce, você decidiu criar um monorepo e compartilhar seu hook como um pacote.

Mas como fazer isso? É o que iremos descobrir.

Como criar o monorepo

Bom, para trabalhar com um único repositório vamos precisar de uma ferramenta que seja confiável e facilite o fluxo de trabalho dentro dele.

Por isso, a escolha óbvia foi o Turborepo. Com o Turborepo, você pode criar, consumir e publicar pacotes, com builds otimizados e uma estrutura modular que favorece a escalabilidade.

E melhor ainda: você pode transformar o seu código em pacotes independentes que podem ser consumidos não apenas dentro do repositório, mas também por outros projetos.

Abra um prompt de comando (cmd) na pasta de sua preferência. Lá você vai digitar o seguinte comando:

npx create-turbo@latest

Ao pressionar a tecla enter vai pedir para você diga qual gerenciador de pacotes irá usar (npm, yarn, pnpm, etc) e também um nome para o monorepo. Escolha um nome de sua preferência, eu vou escolher library-hooks-react.

Você verá uma mensagem como abaixo:

? Where would you like to create your Turborepo? ./library-hooks-react
? Which package manager do you want to use? npm

>>> Creating a new Turborepo with:

Application packages
 - apps\docs
 - apps\web
Library packages
 - packages\eslint-config
 - packages\typescript-config
 - packages\ui

>>> Success! Created your Turborepo at library-hooks-react
To get started:
- Change to the directory: cd library-hooks-react
- Enable Remote Caching (recommended): npx turbo login
   - Learn more: https://turbo.build/repo/remote-cache

- Run commands with Turborepo:
   - npm run build: Build all apps and packages
   - npm run dev: Develop all apps and packages
   - npm run lint: Lint all apps and packages
- Run a command twice to hit cache

Agora você pode acessar a pasta que nomeou no passo anterior. E se preferir, pode até abrir ela no seu editor de código. Eu uso o VS Code, então a minha estrutura de arquivos é esta aqui:

library-hook-react
├── apps
│   ├── docs
│   └── web
├── packages
│   ├── eslint-config
│   └── typescript-config
│   └── ui
├── .gitignore
├── .npmrc
├── package-lock.json
├── package.json
├── README.md
└── turbo.json

O turborepo já cria para a gente uma aplicação com Next.js, ela está dentro da pasta apps.

Na pasta packages são pacotes e dependências que podem ser compartilhados entre as aplicações do mesmo repositório, como componentes de UI, configurações do typescript de lint dos seus projetos.

Duas coisas que podemos fazer neste momento é renomear a propriedade name no package.json para “root-monorepo”, para ficar claro que é da pasta raiz e diferenciar do nome do pacote que iremos publicar (shhh, spoilers…).

Além disso, vamos deletar a pasta apps/docs pois não iremos usá-la aqui.

//library-hooks-react/package.json
{
  "name": "root-monorepo", //altere o name para root-monorepo
  "private": true,
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },
  // outras propriedades
}

Com isso, a primeira etapa que era configurar o monorepo foi concluída. Agora vamos tratar de criar o hook que queremos compartilhar. Vamos lá!

Criando a biblioteca de hooks

Precisamos criar um pacote/workspace com os hooks que queremos compartilhar. Para isso, você precisa criar uma pasta dentro da pasta ./packages com um nome de sua escolha. Eu escolhi “hooks-lib”.

Pelo terminal:

// a partir da raiz acesse a pasta packages
cd packages

// crie uma pasta com o nome de sua escolha
mkdir hooks-lib

// inicialize um novo pacote npm
npm init -y

// instale as dependências que iremos precisar para criar o hook
npm install -D react react-dom @types/react @types/react-dom

Como iremos utilizar o React com Typescript para criar nossa biblioteca de hooks, precisei instalar as dependências do react e os respectivos tipos.

O resultado é uma pasta chamada hooks-lib contendo um package.json e uma pasta node_modules.

Agora precisamos adicionar um tsconfig para criarmos o hook. Então, dentro de hooks-lib eu crio um arquivo tsconfig.json com o seguinte conteúdo:

{
  "extends": "../typescript-config/base.json",
  "include": ["."],
  "exclude": ["dist", "node_modules"],
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"]
  }
}

Lembra que falei que dentro do monorepo conseguimos reutilizar dependências? No código acima temos um exemplo dessa reutilização.

Como o turborepo já cria para a gente toda uma configuração do typescript que fica dentro da pasta ./packages/typescript-config/base.json, extendemos essa configuração no tsconfig.json do nosso pacote. Legal né?

Agora você só precisa reinstalar as dependências para garantir que tudo irá funcionar direitinho. No diretório raiz do projeto, rode o comando: npm install. E prontinho.

Agora precisamos criar o nosso hook. Então dentro de ./packages/hooks-lib eu crio um arquivo index.ts com o seguinte código:

import { useState } from "react";

type Address = {
  logradouro: string;
  bairro: string;
  localidade: string;
  uf: string;
};

export const useCep = () => {
  const [error, setError] = useState<string | null>(null);

  const fetchAddress = async (cep: string): Promise<Address | null> => {
    setError(null);
    try {
      const formattedCep = cep.replace(/\D/g, "");
      if (formattedCep.length !== 8) {
        throw new Error("CEP inválido");
      }
      const response = await fetch(
        `https://viacep.com.br/ws/${formattedCep}/json/`
      );
      const data = await response.json();
      if (data.erro) {
        throw new Error("CEP não encontrado");
      }
      return {
        logradouro: data.logradouro,
        bairro: data.bairro,
        localidade: data.localidade,
        uf: data.uf,
      };
    } catch (err: any) {
      setError(err.message || "Erro ao buscar o CEP");
      return null;
    }
  };

  return { fetchAddress, error };
};

Este arquivo é nosso hook useCep, que a partir de um número de CEP retorna o endereço completo, permitindo o autocomplete de campos de um formulário de endereço, por exemplo.

E agora lá no ./packages/hooks-lib/package.json só precisamos alterar a propriedade main indicando o arquivo que será exportado, no caso o index.ts.

{
  "name": "hooks-lib",
  "description": "",
  "main": "/index.ts",
  // outras propriedades
}

Agora nosso próximo passo será consumir este hook nas aplicações dentro do próprio repositório único. Então vamos fazer isso!

Usando o hook nas aplicações do monorepo

Como o turborepo já criou uma aplicação Next.js para a gente, só vamos fazer alguns ajustes para conseguir usar o hook useCep na aplicação.

  1. Vamos renomear a pasta apps/web para apps/next
  2. Dentro de ./apps/next/package.json vamos mudar a propriedade name para “next”
    {
      "name": "next",
      "version": "0.1.5",
      "type": "module",
      "private": true,
      // outras propriedades
    }

Agora se formos no arquivo ./apps/next/app/page.tsx e tentarmos usar o hook useCep, vamos tomar um erro.

Este erro acontece porque não importamos o hook do caminho correto, então precisamos dizer a nossa aplicação que a dependência que ela está procurando está dentro de um pacote, compartilhado do repositório.

Para isso, vamos lá em ./apps/next/package.json nas dependências, vamos importar nosso pacote:

  "dependencies": {
    "hooks-lib": "workspace:*",
    "next": "^0.1.5",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },

Feito isso, podemos rodar o comando npm install para instalar essa dependência no projeto next. Agora sim já podemos usar nosso hook. Para isso, em ./apps/next/app/page.tsx eu criei o seguinte código:

//./apps/next/app/page.tsx
"use client";
import { useCep } from "hooks-lib";
import styles from "./page.module.css";
import { useState } from "react";

export default function Home(): JSX.IntrinsicAttributes {
  const [formData, setFormData] = useState({
    cep: "",
    logradouro: "",
    bairro: "",
    localidade: "",
    uf: "",
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData((prev) => ({ ...prev, [name]: value }));
  };

  const handleCepBlur = async () => {
    if (formData.cep) {
      const address = await fetchAddress(formData.cep);
      if (address) {
        setFormData((prev) => ({
          ...prev,
          logradouro: address.logradouro,
          bairro: address.bairro,
          localidade: address.localidade,
          uf: address.uf,
        }));
      }
    }
  };

  const { fetchAddress, error } = useCep();
  return (
    <div className={styles.page}>
      <input
        name="cep"
        value={formData.cep}
        onChange={handleChange}
        onBlur={handleCepBlur}
        maxLength={9}
      />
      {error && <p className={styles.error}>Erro: {error}</p>}
      <pre>{JSON.stringify(formData)}</pre>
    </div>
  );
}

Agora você tem duas opções:

  1. Ou você volta a pasta raiz do projeto e digita o comando npm run dev para subir sua aplicação
  2. Você permanece no diretório ./apps/next e digita npm run dev para rodar sua aplicação Next.js

Ambas as opções vão subir sua aplicação Next, com a diferença de que o primeiro comando roda o turbo dev, que dispara um script em cada área de trabalho com um nome específico. É útil quando você quer executar todas as aplicações de uma vez só.

A segunda opção só roda a aplicação Next.js.

Ao subir a aplicação, ela exibe na url http://localhost:3000 um campo de input para digitarmos o CEP, e ao tirar o foco do campo automaticamente um JSON é exibido na página com as informações de endereço do usuário. Legal né?

E está prontinha a nossa biblioteca de hooks. Você pode criar mais hooks, dentro de uma pasta chamada hooks em ./packages/hooks-lib e exportar seus hooks do arquivo index.ts normalmente. Também irá funcionar.

Só tome cuidado na exportação, busque seguir as melhores práticas como adotar o padrão Barrel Export.

Se você não conhece esse padrão de exportação, leia este artigo do Klaus Kazlauskas acessando este link da plataforma Medium.

Só que você não pode deixar que um hook tão interessante e usual fique apenas no seu monorepo, não é? Seu hook pode facilitar a vida de muitos desenvolvedores(as) pelo mundo afora, então o que você pode fazer?

Você pode publicar sua biblioteca de hooks no npm, por exemplo. É isto que iremos fazer agora.

Publicando no npm

Antes de tudo, para você publicar algum pacote no npm você precisa ter uma conta. Se você não tiver uma conta, precisa criar uma. É bem rapidinho, só preencher alguns dados e verificar sua conta através do email.

Outro detalhe super importante aqui é que antes de publicar o pacote você precisa confirmar se o nome que escolheu para o pacote está disponível. Você pode fazer isso digitando no terminal:

npm view <nome-que-voce-escolheu>

Se o nome estiver disponível, você receberá uma mensagem como essa:

npm error code E404
npm error 404 Not Found - GET https://registry.npmjs.org/library-hooks-react - Not found
npm error 404
npm error 404  '<nome-que-voce-escolheu>' is not in this registry.
npm error 404
…

E aí você pode usar este nome para o seu pacote. Caso contrário, aparecerão informações sobre a biblioteca existente com o nome que você iria publicar.

Se você escolheu um nome não disponível, pode mudar o nome do seu pacote trocando a propriedade name em ./packages/hooks-lib/package.json para o nome disponível que você encontrar.

Compilando o Typescript para JavaScript

Nossa biblioteca funciona nas aplicações Vite e Next porque ambas compilam o código Typescript para Javascript.

Mas como queremos publicar o pacote de hooks, precisamos compilar o TypeScript para o JavaScript.

Antes de publicar precisamos fazer algumas etapas:

  1. Agrupar e compilar o código-fonte para esm e commonjs, para quem instalar o nosso package ser capaz de importá-lo;
  2. Configurar um script de desenvolvimento para desenvolvimento local;
  3. Publicar no NPM; 3.1. Realizar o controle de versionamento; 3.2. Realizar o controle de publicação.

Para compilar nosso código TS para JS vamos utilizar o tsup.

O tsup é uma ferramenta de empacotamento e compilação de TypeScript que gera bundles em formatos como CJS e ESM, além de criar arquivos de declaração de tipos (.d.ts).

Ele oferece uma configuração simples e performance rápida, ideal para bibliotecas e projetos que compartilham um único repositório.

Precisamos ir na pasta ./packages/hooks-lib e digitar no terminal

npm install -D tsup

Agora, precisamos criar uma pasta src e mover o arquivo index.ts para dentro dela. E como parte de criar um script de desenvolvimento local, vamos no package.json do hooks-lib (./packages/hooks-lib/package.json) e adicionamos este script:

"build": "tsup src/index.ts --format esm,cjs --dts"

Este script, vai usar tsup para agrupar e compilar nosso código, e também saídas de arquivos para o diretório dist.

Agora vamos ajustar o main no ./packages/hooks-lib/package.json, para quem instalar esses pacotes usar a versão empacotada e compilada do código.

"main": "./dist/index.js",
"types": "./dist/index.d.js",

Um detalhe importante aqui, é: Não queremos injetar todo o react e as dependências do pacote quando alguém usar a biblioteca de hooks.

Quem usar ela já estará usando o react, então podemos adicionar essas dependências nas dependências por pares, assim:

  "peerDependencies": {
    "react": ">=18.2.0",
    "react-dom": ">=18.2.0"
  }

E antes, os apps importavam diretamente os arquivos .ts e refletiam as alterações que fazíamos na biblioteca automaticamente.

Agora com o tsup, é necessário rodar o build manualmente ao atualizar o código. Esta etapa irá gerar o nosso hook compilado em Javascript, bem como seus tipos.

Agora atualize o package.json em ./packages/hooks-lib/package.json para corrigir.

"dev": "npm run build --watch"

E o arquivo package.json completo tem essa cara:

{
  "name": "library-hooks-react",
  "version": "1.0.0",
  "description": "",
  "main": "./dist/index.js",
  "types": "./dist/index.d.js",
  "scripts": {
    "build": "tsup src/index.ts --format esm,cjs --dts",
    "dev": "npm run build --watch"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/react": "^19.0.1",
    "@types/react-dom": "^19.0.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "tsup": "^8.3.5"
  },
  "peerDependencies": {
    "react": ">=18.2.0",
    "react-dom": ">=18.2.0"
  }
}

Controlando as versões com changeset

Já que estamos publicando no npm a nossa lib, é importante manter sempre um controle de versões conforme ela for crescendo e mudanças forem sendo realizadas, como a adição de novos hooks, melhorias de código, etc.

Para fazer este controle de versão e publicação, vamos usar a biblioteca changesets.

O Changesets é uma ferramenta que automatiza o versionamento e o changelog de pacotes, gerenciando as versões com base nas mudanças realizadas.

Então vamos configurar o changesets no nosso projeto. Começando com a instalação. No terminal, do diretório raiz do projeto (./library-hooks-react) digite:

// instalação
npm i @changesets/cli

E agora faça a inicialização do changesets

// setup
npx changeset init

Você verá uma mensagem como esta:

Thanks for choosing changesets to help manage your versioning and publishing
🦋
🦋  You should be set up to start using changesets now!
🦋
🦋  info We have added a `.changeset` folder, and a couple of files to help you out:
🦋  info - .changeset/README.md contains information about using changesets
🦋  info - .changeset/config.json is our default config

Agora vamos criar nossas versões. Dentro de ./packages/hooks-lib/package.json mude a versão do seu pacote:

"version": "0.0.1",

E agora na raiz do monorepo vamos usar o changeset:

npx changeset

Vai aparecer uma sequência de mensagens. A primeira pergunta qual pacote você quer incluir:

🦋  Which packages would you like to include? ...
( ) unchanged packages
  ( ) next
  ( ) vite-react
  ( ) @repo/eslint-config
  (*) library-hooks-react
  ( ) @repo/typescript-config
  ( ) @repo/ui

Você deve selecionar o pacote em questão com a tecla “espaço” e depois pressionar “enter” para prosseguir.

🦋  Which packages should have a major bump? ...
(*) all packages
  (*) [email protected]

Selecione o pacote com a versão específica. A sua deve ser também a 0.0.1. Cheque bem isso.

Faça uma pequena descrição dessa primeira versão na etapa seguinte:

🦋  Summary »  Primeiro release

// Você verá esta mensagem após confirmar:
🦋  
🦋  === Summary of changesets ===
🦋  major:  library-hooks-react
🦋  
🦋  Note: All dependents of these packages that will be incompatible with the new version will be patch bumped when this changeset is applied.

Na última etapa você só precisa confirmar:

🦋  Is this your desired changeset? (Y/n) » true

Você pode rodar o comando npx changeset version para preparar sua versão. Note que após este comando a propriedade version em ./packages/hooks-lib/package.json muda para 1.0.0.

Agora se certifique que dentro de cada workspace, onde tem um arquivo package.json, o valor da propriedade private esteja como true. Isso diz quais pacotes você NÃO QUER publicar.

Agora lembra lá do diretório ./changeset? Precisamos fazer algumas mudanças no config.json dentro deste diretório. Na propriedade access, vamos alterar de restricted para public.

// antes
"access": "restricted",

// depois
"access": "public",

E agora finalmente podemos publicar nossa biblioteca no npm. Rode os comandos abaixo na raiz do seu diretório:

// faça login no npm. Se já estiver logado pule esta etapa
npm login

// publique sua biblioteca com o changeset
npx changeset publish

E se tudo correr bem você deve ser capaz de ler esta mensagem no console:

info npm info library-hooks-react-turbopack
//...
info Publishing "library-hooks-react" at "1.0.0"
success packages published successfully:
library-hooks-react1.0.0
Creating git tag...
New tag: [email protected]

E dessa forma você já tem sua biblioteca de hooks react publicada também no npm.

Agora qualquer pessoa desenvolvedora pode instalar e usar o seu pacote de hooks. Bem maneiro né? Você pode acessar o npm e ver lá no seu perfil, na opção packages o novo pacote que você acabou de publicar.

Banner promocional da Alura, com chamada para um evento ao vivo no dia 12 de fevereiro às 18h30, com os dizeres

Conclusão

Compartilhar código por meio de uma biblioteca de hooks em um monorepo com Turborepo, traz benefícios, como reusabilidade e integração entre diferentes aplicações.

Isso facilita a manutenção e acelera o desenvolvimento, permitindo que componentes e lógicas sejam reaproveitados.

E ao publicar no npm esses recursos você colabora com a comunidade React, disponibilizando uma biblioteca que abstrai uma lógica muito usada em diferentes projetos, como o caso do useCep.

Se você quiser se aprofundar em como trabalhar e desenvolver aplicações que compartilham um único repositório, pode acessar o link da formação Construa um Design System com React, Turborepo e Storybook e se você for do time Angular, pode também fazer a formação Angular: cursos para construir um Design System com Nx, Monorepo e Storybook.

Referências

Neilton Seguins
Neilton Seguins

Sou graduado como Bacharel em Ciência e Tecnologia e em Engenharia Mecânica. Atuo como Instrutor de Desenvolvedor de Software na Alura e possuo experiência com desenvolvimento usando JavaScript/TypeScript, React js, Next js e Node.js. Amo compartilhar conhecimento, pois acredito que a educação é transformadora e quero mudar a vida de pessoas através da educação assim como consegui mudar de vida. Também amo ouvir e tocar música, ler livros e mangás e assistir séries.

Veja outros artigos sobre Front-end