Desarrollar componentes React con TypeScript y sistemas de diseño con Storybook

Escrito por picodotdev el , actualizado el .
javascript planeta-codigo web
Enlace permanente Comentarios

Con Storybook los componentes de React, Vue o Angular es posible desarrollarlos de forma aislada sin necesidad de hacerlo una una de las aplicaciones finales donde se usen. Esto permite independizar su desarrollo de las aplicaciones finales y proporciona un entorno donde hacerlo. Con complementos permite realizar pruebas unitarias y pruebas visuales.

La web ha evolucionado enormemente desde las simples páginas estáticas con contenido HTML, imágenes y hojas de estilos. Con posterioridad se añadió un lenguaje de programación en el navegador del lado del cliente para realizar tareas en las propias páginas como validaciones de formulario. A medida que el tiempo ha pasado los navegadores han implementado nuevos estándares y a través de JavaScript ahora hay posibilidad de desarrollar tareas en el lado de cliente que rivalizan con las aplicaciones tradicionales de escritorio.

Algunas de estas nuevas capacidades de JavaScript son nuevas versiones del lenguaje con ECMAScript con soporte para módulos, WebGL o componentes de lado de cliente con Web Components. Con las nuevas capacidades de JavaScript han surgido una comunidad de JavaScript con numerosas librerías entre las que elegir para realizar tareas. Una de las áreas son los componentes de lado del cliente, el estándar que define la W3C son los Web Components pero hay algunas otras alternativas que sustituyen o complementan como React, Vue o Angular.

Para desarrollar componentes en lado del cliente se necesita la aplicación final donde se van a usar, si se está desarrollando una librería para ser usada en múltiples aplicaciones de una organización o incluso un sistema de diseño o design system para la organización es muy útil poder desarrollar, probar y ejecutar estos componentes de forma aislada de la aplicación donde se usen.

En este artículo muestro cómo utilizar Storybook, componentes React con TypeScript, pruebas unitarias con Jest y visuales con Jest Image Snapshot, archivos CSS con Less finalmente como crear un paquete de npm para utilizarlo en otra librería o proyecto.

Desarrollo aislado de componentes, sistemas de diseño y documentación con Storybook

Storybook es una herramienta que permite desarrollar los componentes de React, Vue o Angular entre otros de forma aislada, es como una caja de arena donde desarrollarlos, probarlos y además documentarlos. El desarrollo incluye las pruebas unitarias con Jest y visual testing con un complemento para Jest que permite si con algún cambio ha habido alguna variación en el aspecto visual de un componente a nivel de píxel. También permite ver el comportamiento de los componentes en diseños resposive y ver su documentación así como en diferentes configuraciones.

Storybook puede utilizarse para implementar un design system de una organización y ver los diferentes colores, estilos y componentes en ejecución y no solo como un diseño. Esto hace que el diseño y la implementación del diseño se mantengan sincronizados y no surjan inconsistencias entre ellos.

En su guía de inicio un comando permite crear la estructura de archivos para empezar a usarlo.

1
$ npx -p @storybook/cli sb init --type react
storybook-create.sh

Con otro comando se inicia un servidor que genera la página web para Storybook. Por defecto hay dos historias o stories con dos componentes de React propios de Storybook. Las stories son las definiciones de las variaciones de los componentes o del design system.

1
$ npm run storybook
storybook-run.sh

Código de definición de una historia.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import React from 'react';
import { linkTo } from '@storybook/addon-links';
import { Welcome } from '@storybook/react/demo';

export default {
  title: 'Welcome',
  component: Welcome,
};

export const ToStorybook = () => <Welcome showApp={linkTo('Button')} />;

ToStorybook.story = {
  name: 'to Storybook',
};
0-Welcome.stories.tsx

Según los parámetros de los componentes estos tiene variaciones, en el ejemplo si se indica un parámetro muestra un mensaje por defecto si se le pasa un parámetro con un nombre muestra un mensaje con ese nombre.

Historia de bienvenida y componente HelloWorld

Storybook ofrece dos formas de desarrollar las stories, en formato Component Story Format o CSF o con la sintaxis MDX que es similar a Markdown con algunas cosas adicionales para poder añadir visualizaciones de componentes. El formato MDX permite añadir texto y documentar con descripciones las stories.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import HelloWorld from '../src/components/HelloWorld';

export default {
  title: 'HelloWorld',
  parameters: {
    componentSubtitle: 'Componente básico de ejemplo',
  },
  component: HelloWorld,
};

export const HelloWorldStory = () => <HelloWorld />;
export const HelloNameStory = ({ name: String }) => <HelloWorld name="picodotdev"/>;

HelloWorldStory.story = {
  name: 'HelloWorld',
};

HelloNameStory.story = {
  name: 'AnotherHelloWorld',
};

HelloWorld.stories.tsx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
import { text } from '@storybook/addon-knobs';

import HelloWorld from '../src/components/HelloWorld';

<Meta title="Components|HelloWorld" component={HelloWorld} />

# HelloWorld

With `MDX` we can define a story for `HelloWorld` right in the middle of our
markdown documentation.

<Preview>
  <Story name="HelloWorld">
    <HelloWorld />
  </Story>
</Preview>

<Preview>
  <Story name="HelloName">
    <HelloWorld name={text("name", "picodotdev")}/>
  </Story>
</Preview>

<Props of={HelloWorld} />
HelloWorld.stories.mdx

En la web se pueden consultar varios ejemplos de Storybook que han desarrollado otras organizaciones y obtener una muestra de su utilidad.

Storybook posee varios complementos que le añaden nuevas capacidades. Algunos son:

  • @storybook/addon-docs: permite desarrollar historias en formato MDX.
  • @storybook/addon-viewport: permite probar las historias aplicando diseños responsive.
  • @storybook/addon-knobs/register: permite modificar propiedades de los componentes desde Storybook y observar los cambios en tiempo real.

Para usar un complemento hay que instalar su paquete y añadirlo en el archivo de configuración.

1
$ npm install --save-dev @storybook/addon-docs @storybook/addon-storysource @storybook/addon-viewport @storybook/addon-knobs
npm-install-storybook-addons.sh
1
2
3
4
module.exports = {
  stories: ['../stories/**/*.stories.(tsx|mdx)'],
  addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/preset-typescript', '@storybook/addon-docs', '@storybook/addon-viewport', '@storybook/addon-knobs/register']
};
main.js

Lenguaje de programación TypeScript y TSLint

TypeScript es un superconjunto de JavaScript, su principal diferencia es que es un lenguaje tipado lo que permite descubrir en tiempo de compilación numerosos errores que se producirán en tiempo de ejecución y que los editores ofrecen soporte realizar refactors de forma rápida y segura. Los componentes de React pueden desarrollarse con TypeScript.

Para desarrollar un componente propio basta con crear el archivo del componente en la carpeta src/components con la definición del componente en este caso de React. Con React se incluye el soporte para desarrollar componentes con ES2016 y JSX de React, a continuación se muestra usando TypeScript y archivos TSX que es el equivalente de JSX en TypeScript.

Para añadir el soporte de TypeScript a Storybook hay que instalar algunos paquetes npm, crear algún archivo de configuración y realizar modificaciones en otros. Además de TypeScript se añade el paquete para utilizar el linter TSLint para este lenguaje que muestra errores en caso de no cumplir las convenciones y reglas de formateo.

1
$ npm install --save-dev @storybook/preset-typescript typescript ts-loader tslint @types/node @types/react @types/react-dom
typescript-install.sh

Este archivo de configuración especifica las opciones para la compilación entre ellas se indica el formato de la salida, los archivos de código fuente ts y tsx son compilados a JavaScript de ECMAScript 5.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  "compilerOptions": {
    "outDir": "build/typescript",
    "module": "commonjs",
    "target": "es5",
    "lib": ["es5", "es6", "es7", "es2017", "dom"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDirs": ["src", "stories"],
    "baseUrl": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": true,
    "strictNullChecks": false,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "declaration": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build", "scripts", "src/**/*.test.*", "src/**/*.test-visual.*"]
}
tsconfig.json

Este es el código de un componente propio de React sencillo programado con TypeScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from 'react';
import PropTypes from 'prop-types';

import './HelloWorld.less';

interface Props {
  name?: String;
}

class HelloWorld extends React.Component<Props> {
  public static defaultProps = {
    name: "World"
  };

  public static propTypes = {
    name: PropTypes.string,
  };

  render() {
    return <h1 className="helloworld-title--red">Hello {this.props.name}!</h1>;
  }
}

export { HelloWorld as HelloWorld };

/**
 * Componente sencillo de ejemplo.
 */
export default HelloWorld;
HelloWorld.tsx
1
2
3
.helloworld-title--red {
    color: red;
}
HelloWorld.less

Para analizar y validar el formato del código fuente se suelen emplear un linter que muestra un conjunto de mensajes que el código fuente no cumple. Estos mensajes son muy útiles para mantener la uniformidad en el código fuente y una forma automatizada de comprobar las reglas.

1
2
3
4
5
6
7
8
9
{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {},
    "rulesDirectory": []
}
tslint.json
1
$ npm run tslint
tslint-run.sh

Pruebas unitarias y visuales con Jest y Jest Image Snapshot

En los tiempos actuales desarrollar pruebas debería ser parte del desarrollo, Jest permite realizar pruebas unitarias y jest-image-snapshot para realizar pruebas visuales. Hay instalar los paquetes de estas herramientas y añadir varios archivos de configuración, las pruebas también pueden desarrollarse con TypeScript, hay que añadir varios archivos de configuración.

1
$ npm install --save-dev @storybook/addon-storyshots-puppeteer jest puppeteer jest-puppeteer jest-image-snapshot jest-transform-stub puppeteer-extra start-server-and-test ts-jest @types/jest @types/puppeteer @types/jest-environment-puppeteer @types/expect-puppeteer
jest-install.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module.exports = {
    transform: {
        '.(ts|tsx)': 'ts-jest',
	    '.(less)': 'jest-transform-stub'
    },
    moduleNameMapper: {
        ".(less)": "jest-transform-stub"
    },
    testRegex: './*\\.test\\.(ts|tsx)$',
    moduleFileExtensions: ['js', 'tsx', 'json']
};
jest.config.js

Para el componente anterior la definición de la prueba unitaria es la siguiente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDOM from 'react-dom';

import { HelloWorld } from './HelloWorld';

it('has a h1 tag with text', () => {
  const div: HTMLDivElement = document.createElement('div');
  ReactDOM.render(
    <HelloWorld />,
    div
  );

  expect(div.querySelector('h1')).not.toBeNull();
  expect(div.querySelector('h1').textContent).toEqual('Hello World!');

  ReactDOM.unmountComponentAtNode(div);
});
HelloWorld.test.tsx

Algunos cambios que afectan a los componentes son simplemente visuales como color, tamaño de letra, espaciado, … estos cambios son difíciles de probarlos con pruebas unitarias de código. Para validar estos cambios la estrategia que se emplea es generar una imagen inicial del componente, cuando hay cambios visuales se genera un error y hay que validar visualmente que el cambio es correcto. Esto permite que los cambios visuales no pasen desapercibidos. Para realiza la validación jest-image-snapshot proporciona la imagen de la versión anterior, la imagen nueva y una imagen que muestra las diferencias entre ambas versiones.

Estos son archivos de configuración para Jest.

1
2
3
import { toMatchImageSnapshot } from 'jest-image-snapshot';

expect.extend({ toMatchImageSnapshot });
jest.setup.js
1
2
3
4
5
module.exports = {
    preset: 'jest-puppeteer',
    testRegex: './*\\.test-visual\\.ts$',
    setupFilesAfterEnv: ['./jest.setup.js']
};
jest.config-visual.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        }
      }
    ],
    '@babel/preset-react',
  ],
};
babel.config.js

El código de la prueba visual requiere incluir interactuar con el navegador donde está contenido el componente en la prueba y especificar el momento en el que tomar la imagen visual de componente.

1
2
3
4
5
6
7
8
9
import puppeteer from 'puppeteer-extra';

it('visually looks correct', async () => {
    await page.goto('http://localhost:6006/iframe.html?selectedKind=components-helloworld&selectedStory=hello-world');

    const image = await page.screenshot();

    expect(image).toMatchImageSnapshot();
});
HelloWorld.test-visual.ts

En la imagen a revisar se muestra a la izquierda la versión anterior válida, a la derecha la nueva imagen por cambios realizados y en el centro una imagen que resalta las diferencias entre ambas a nivel de pixel. Con estas tres imágenes la revisión es un proceso manual pero sin complicación.

Imagen válida capturada y diferencias visuales por cambios

Para lanzar las tareas de ejecución de las pruebas unitarias y visuales hay que añadir unos scripts al archivo package.json. Posteriormente con estos comandos de npm se ejecutan y se comprueba si hay cambios visuales.

1
$ npm run test
test-run.sh
1
$ npm run test:visual
test-visual-run.sh

En caso de haber diferencias visuales al ejecutar de nuevo los teses se produce un error, hay que revisar visualmente los cambios y si son correctos validar y actualizar las imágenes para ejecuciones futuras.

1
$ npm run jest:visual-update
test-visual-update.sh

Hojas de estilos CSS con Less

Las hojas de estilo CSS permite separar el formato del contenido HTML. Para facilitar el desarrollo de hojas de estilo han surgido herramientas que añaden capacidades que CSS no posee. Estas herramientas como Less permiten generar como resultado un archivo CSS. Hay múltiples herramientas Less es una de ellas que se caracteriza por su simplicidad.

Storybook permite utilizar archivos de hojas de estilo less. Para realizar la transformación de less a css Storybook utiliza Webpack, hay que instalar las dependencias que le permiten transformar los archivos y la configuración para que detecte los archivos less para transformarlos a css.

1
$ npm install --save-dev less less-loader style-loader css-loader
npm-install-webpack.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module.exports = {
  module: {
    rules: [
      { test: /\.(ts|tsx)$/, use: 'ts-loader' },
      { test: /\.less$/, use: [
        { loader: 'style-loader' },
        { loader: 'css-loader' },
        { loader: 'less-loader' }
      ]}
    ]
  },
  resolve: {
    extensions: ['.ts', '.tsx']
  }
};
webpack.config.js

Creación del paquete NPM

El objetivo final es crear un paquete npm que incluya los componentes de React para ser utilizados en una aplicación. Para crear el paquete npm basta ejecutar el comando npm pack pero este lo crea usando la misma estructura de directorios del código fuente lo que hace que al usar los componentes los imports reflejen la estructura del código fuente. Si esto no se desea hay que crear un directorio con el contenido del paquete npm y ejecutar el comando npm pack desde él, esto es lo que hacen los diferentes scripts build.

Otros scripts contiene el comando real que se ejecuta cuando se invoca desde la linea de comandos con npm run [script], entre ellos están los de Jest y Storybook.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{
  "name": "storybook",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "types": "index.d.ts",
  "scripts": {
    "build:create-package": "mkdir -p build/pack/",
    "build:copy-package": "cp package.json build/pack/",
    "build:typescript": "tsc",
    "build:copy-typescript": "cp -r build/typescript/* build/pack/",
    "build:copy-src": "(cd src/ && cp --parents `find -name \\*.less` ../build/pack/)",
    "build:pack": "(cd build/pack && npm pack)",
    "pack": "npm run build:create-package && npm run build:copy-package && npm run build:typescript && npm run build:copy-typescript && npm run build:copy-src && npm run build:pack",
    "test": "jest -c jest.config.js",
    "test:visual": "start-server-and-test storybook http-get://localhost:6006 jest:visual",
    "jest:visual": "jest -c jest.config-visual.js",
    "jest:visual-update": "npm run jest:visual -- --updateSnapshot",
    "lint": "tslint -c tslint.json 'src/**/*.ts'",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^16.13.0"
  },
  "files": [
    "**/*.js",
    "**/*.js.map",
    "**/*.d.ts",
    "**/*.less"
  ],
  "devDependencies": {
    "@babel/core": "^7.8.6",
    "@storybook/addon-actions": "^5.3.14",
    "@storybook/addon-docs": "^5.3.14",
    "@storybook/addon-knobs": "^5.3.14",
    "@storybook/addon-links": "^5.3.14",
    "@storybook/addon-storyshots-puppeteer": "^5.3.14",
    "@storybook/addon-storysource": "^5.3.14",
    "@storybook/addon-viewport": "^5.3.14",
    "@storybook/addons": "^5.3.14",
    "@storybook/preset-typescript": "^1.2.0",
    "@storybook/react": "^5.3.14",
    "@types/expect-puppeteer": "^4.4.0",
    "@types/jest": "^25.1.3",
    "@types/jest-environment-puppeteer": "^4.3.1",
    "@types/node": "^13.7.7",
    "@types/puppeteer": "^2.0.1",
    "@types/react": "^16.9.23",
    "@types/react-dom": "^16.9.5",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.4.2",
    "jest": "^25.1.0",
    "jest-image-snapshot": "^2.12.0",
    "jest-puppeteer": "^4.4.0",
    "jest-transform-stub": "^2.0.0",
    "less": "^3.11.1",
    "less-loader": "^5.0.0",
    "puppeteer": "^2.1.1",
    "puppeteer-extra": "^3.1.9",
    "react-docgen-typescript-loader": "^3.6.0",
    "start-server-and-test": "^1.10.8",
    "style-loader": "^1.1.3",
    "ts-jest": "^25.2.1",
    "ts-loader": "^6.2.1",
    "tslint": "^6.0.0",
    "typescript": "^3.8.3"
  }
}
package.json

Un paquete npm es un archivo con extensión .tgz que en el ejemplo se crea en el directorio build/pack.

Una vez construido el paquete .tgz para instalarlo en los proyectos donde se quiera usar hay que utilizar el siguiente comando y hacer el import de sus recursos.

1
$ npm install --save-dev build/pack/storybook-1.0.0.tgz
npm-install-package.sh

Storybook es una gran ayuda para desarrollar componentes de lado de cliente. Otras herramienta útil es Webpack como empaquetador de módulos y recursos web de todos lo recursos que se usan en un proyecto. Al utilizar TypeScript no es necesario utilizar Babel para realizar transformaciones de los archivos a una versión de JavaScript que los navegadores soportan, el compilador de TypeScript permite compilar los archivos de TypeScript a diferentes versiones de JavaScript que son las que finalmente se ejecutan en el navegador.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
npm install && npm run storybook

Comparte el artículo: