Cómo construir y publicar una librería de hooks para React con TypeScript y Vite

Más de una vez he escrito hooks de React que quiero convertir en una librería, pero el proceso me ha atascado lo suficiente como para abandonar la idea por completo.

Y es que no tenía del todo claro cómo construir algo así hasta que me di la tarea.

En este artículo te explicaré cómo puedes crear una librería de hooks para React con TypeScript y Vite sin muchas complicaciones.

Crea la base de tu librería

Paso #1: Instalación de Vite y dependencias necesarias

Primero instala Vite usando npm, yarn o pnpm:

npm create vite@latest
yarn create vite
pnpm create vite

Recuerda seleccionar React y TypeScript.

Ahora instala el siguiente plugin de Vite usando npm, yarn o pnpm:

npm install -D vite-plugin-dts
yarn add --dev vite-plugin-dts
pnpm add --save-dev vite-plugin-dts

Paso #2: Configuración de Vite

En la configuración de Vite se usará el modo de librería que se emplea precisamente para construir librerías.

Abre el archivo vite.config.ts y aplica la siguiente configuración:

import react from '@vitejs/plugin-react'
import path from 'node:path'
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    react(),
    dts({
      insertTypesEntry: true,
    }),
  ],
  build: {
    lib: {
      entry: path.resolve(__dirname, 'src/hooks/index.ts'),
      name: 'ejemplo-react-hooks',
      formats: ['es', 'umd'],
      fileName: (format) => `ejemplo-react-hooks.${format}.js`,
    },
    rollupOptions: {
      external: ['react', 'react-dom', 'styled-components'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
          'styled-components': 'styled',
        },
      },
    },
  },
})

En la primera parte se configuran dos plugins para Vite.

El primero es el plugin de React, que permite a Vite manejar proyectos de React. El segundo es vite-plugin-dts, que genera archivos de declaración de TypeScript (.d.ts) para el proyecto.

plugins: [
    react(),
    dts({
      insertTypesEntry: true,
    }),
  ],

Luego se configura cómo Vite debe construir el proyecto.

En este caso se está configurando para construir una librería a partir de los archivos en la ruta src/hooks/index.ts. El nombre de la librería será ejemplo-react-hooks y se generará en los formatos es y umd. Los nombres de los archivos de salida se basarán en el formato.

  lib: {
    entry: path.resolve(__dirname, "src/hooks/index.ts"),
    name: "ejemplo-react-hooks",
    formats: ["es", "umd"],
    fileName: (format) => `ejemplo-react-hooks.${format}.js`,
  },

A continuación, se configuran opciones adicionales para Rollup, el empaquetador de módulos que Vite utiliza internamente.

Se especifican react, react-dom y styled-components como dependencias externas, lo que significa que no se incluirán en el paquete final.

Además, se definen los nombres globales para estas dependencias en el paquete final.

rollupOptions: {
  external: ["react", "react-dom", "styled-components"],
  output: {
    globals: {
      react: "React",
      "react-dom": "ReactDOM",
      "styled-components": "styled",
    },
  },
},

Paso #3: Configuración del package.json

Al abrir el archivo package.json te debes encontrar algo así:

{
  "name": "ejemplo-react-hooks",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@typescript-eslint/eslint-plugin": "^6.14.0",
    "@typescript-eslint/parser": "^6.14.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.55.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "typescript": "^5.2.2",
    "vite": "^5.0.8",
    "vite-plugin-dts": "^3.6.4"
  }
}

Lo primero que debes hacer es pasar las dependencias “react” y “react-dom” como dependencias de desarrollo quedándote así el package.json:

{
  "name": "ejemplo-react-hooks",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@typescript-eslint/eslint-plugin": "^6.14.0",
    "@typescript-eslint/parser": "^6.14.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.55.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "typescript": "^5.2.2",
    "vite": "^5.0.8",
    "vite-plugin-dts": "^3.6.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

Ahora tienes que añadir algunos aspectos.

Comienza con los metadatos de la librería. Incluye el nombre, la descripción, el repositorio, las palabras clave, el autor, la licencia, si es privado o no y la versión. También especifica que el proyecto es un módulo de ES.

"name": "ejemplo-react-hooks",
"description": "Una librería de hook para React de ejemplo",
"repository": {
  "type": "git",
  "url": "https://github.com/dlcastillop/ejemplo-react-hooks"
},
"keywords": [
  "react",
  "hooks",
  "ejemplo"
],
"author": "Daniel Castillo <[email protected]>",
"license": "MIT",
"private": false,
"version": "0.1.0",
"type": "module",

Ten en cuenta que si defines la librería como privada tienes que pagar una mensualidad para alojarla en npm.

Luego define los puntos de entrada para el módulo, el módulo ES y los tipos de TypeScript.

"main": "dist/ejemplo-react-hooks.umd.js",
"module": "dist/ejemplo-react-hooks.es.js",
"types": "dist/ejemplo-react-hooks.d.ts",

Nota que aquí el nombre de los archivos, o sea, ejemplo-react-hooks, tiene que coincidir con el nombre que definiste en la siguiente parte de la configuración de Vite:

lib: {
    entry: path.resolve(__dirname, "src/hooks/index.ts"),
    name: "ejemplo-react-hooks",
    formats: ["es", "umd"],
    fileName: (format) => `ejemplo-react-hooks.${format}.js`,
  },

Esto es porque Vite genera estos archivos a partir de la configuración definida y si en el package.json no usamos el mismo nombre estaríamos proporcionando un archivo que no existe.

Ahora define las exportaciones del módulo. Esta parte especifica cómo otros módulos pueden importar este módulo.

"exports": {
    ".": {
      "import": "./dist/ejemplo-react-hooks.es.js",
      "require": "./dist/ejemplo-react-hooks.umd.js",
      "types": "./dist/ejemplo-react-hooks.d.ts",
      "default": "./dist/ejemplo-react-hooks.es.js"
    }
  },

Una vez más, recuerda que el nombre de los archivos tiene que coincidir con el nombre que definiste en la configuración de Vite.

Por último especifica qué archivos se deben incluir en el paquete cuando se publica en npm.

"files": [
  "dist",
  "package.json"
]

Al final el package.json queda así:

{
  "name": "ejemplo-react-hooks",
  "description": "Una librería de hook para React de ejemplo",
  "repository": {
    "type": "git",
    "url": "https://github.com/dlcastillop/ejemplo-react-hooks"
  },
  "keywords": ["react", "hooks", "ejemplo"],
  "author": "Daniel Castillo <[email protected]>",
  "license": "MIT",
  "private": false,
  "version": "0.1.0",
  "type": "module",
  "main": "dist/ejemplo-react-hooks.js",
  "module": "dist/ejemplo-react-hooks.es.js",
  "types": "dist/ejemplo-react-hooks.d.ts",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@typescript-eslint/eslint-plugin": "^6.14.0",
    "@typescript-eslint/parser": "^6.14.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.55.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "typescript": "^5.2.2",
    "vite": "^5.0.8",
    "vite-plugin-dts": "^3.6.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "exports": {
    ".": {
      "import": "./dist/ejemplo-react-hooks.es.js",
      "require": "./dist/ejemplo-react-hooks.umd.js",
      "types": "./dist/ejemplo-react-hooks.d.ts",
      "default": "./dist/ejemplo-react-hooks.es.js"
    }
  },
  "files": ["dist", "package.json"]
}

Paso #4: Define el archivo .npmignore

Por si no lo conoces, el archivo .npmignore es utilizado por npm como una guía para determinar qué archivos o directorios deben ser ignorados cuando se publica un paquete.

Es similar al archivo .gitignore en su sintaxis y comportamiento, pero sólo afecta a la publicación de paquetes npm, no a los commits de Git.

En este caso, el .npmignore tiene que tener la misma configuración que el archivo .gitignore con la única diferencia que el primero no puede ignorar la carpeta dist.

Esto es porque a la hora de publicar en npm necesitas que la carpeta dist se suba.

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

Sáltate los primeros pasos

Estas configuraciones que te acabo de explicar pueden ser un poco tediosas.

Es por eso que creé Hook Crafter: una plantilla para construir tu propia librería de hooks para React más rápido.

Hook Crafter está pensado para que descargues el repositorio, hagas unos pequeños ajustes y puedas comenzar más rápido a desarrollar tu librería de hooks.

Es completamente gratis y de código abierto.

Crea tus hooks

En la siguiente parte de la configuración de Vite se definió donde es el punto de entrada de los hooks, o sea, el directorio src/hooks/index.ts:

lib: {
    entry: path.resolve(__dirname, "src/hooks/index.ts"),
    //...
  },

Esto significa que tienes que crear un archivo llamado index.ts en la ubicación especificada y este se debe encargar de exportar los hooks.

Cuando se construye la librería, Vite tomará todas las funciones exportadas en index.ts y las incluirá en la librería final para que cualquier lugar que importe esta librería tenga acceso a estas funciones.

Para poner un ejemplo voy a crear un hook sencillo que incremente un contador.

import { useState } from 'react'

export const useCountUp = (increase: number) => {
  const [count, setCount] = useState(0)

  const increment = () => setCount(count + increase)
  return { count, increment }
}

Ahora en el index.ts exporto este hook:

export * from './useCountUp '

Repite este proceso con cada uno de los hooks que tu librería contendrá.

Publica tu librería

Antes de publicar tienes que generar la carpeta dist con npm, yarn o pnpm:

npm run build
yarn build
pnpm build

Ahora necesitas una cuenta de npm. Si no tienes una, te la puedes crear en su sitio web.

Una vez tengas la cuenta, entra a npm desde tu terminal con npm login y sigue las instrucciones.

Finalmente publica tu librería al ejecutar npm publish y listo.