DEV Community

Eduardo Henrique Gris
Eduardo Henrique Gris

Posted on

Biblioteca de componentes React e typescript, parte 5: documentação de componentes com storybook

Introdução

English version: React and typescript components lib, part 5: documentation of components using storybook

Na parte cinco, será adicionada a documentação dos componentes usando storybook, com o objetivo de mostrar a partir dela como funciona os componentes e as diferentes customizações possíveis para eles. A ideia é mostrar a parte prática da adição do storybook nesse artigo e como realizar a documentação, para uma referência de como funciona a lib segue o artigo que escrevi anteriormente de referência: Documentação de componentes em React com Typescript usando Storybook (versão 8).

Setup

Primeiro será realizado o setup com as configurações inicias do storybook, a partir da execução no terminal: npx storybook init.
Esse comando vai detectar as dependências do projeto e apresentar três passos:

  • informar qual versão do storybook vai ser adicionada
  • perguntar se o projeto usa webpack ou vite
  • perguntar se vai usar o storybook para documentação ou teste

Quanto ao primeiro ponto é só prosseguir. Do segundo ponto, o projeto usa rollup, então não seria na prática nenhuma das duas opções, mas como o storybook não tem integração direta com rollup, será escolhida a opção vite, que vai ser utilizado para rodar o storybook. Da última pergunta, é informar que vai ser usado para documentação.
Ao finalizar o processo vão ser adicionadas automaticamente libs que permitirão executar o storybook.
No momento desse artigo gerou as seguintes versões:

"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-interactions": "^8.6.12",
"@storybook/addon-onboarding": "^8.6.12",
"@storybook/blocks": "^8.6.12",
"@storybook/react": "^8.6.12",
"@storybook/react-vite": "^8.6.12",
"@storybook/test": "^8.6.12",
"eslint-plugin-storybook": "^0.12.0",
"storybook": "^8.6.12",
Enter fullscreen mode Exit fullscreen mode

E em package.json serão adicionados automaticamente dois scripts:

"scripts": {
  "storybook": "storybook dev -p 6006",
  "build-storybook": "storybook build"
},
Enter fullscreen mode Exit fullscreen mode

O primeiro executa o storybook localmente, o segundo faz o build do storybook.

E vai gerar uma pasta .storybook, com dois arquivos configurados para permitir a execução do storybook: main.ts e preview.ts.
Além de uma pasta stories, que possui alguns exemplos de documentação prontos, mas como não vamos utilizar vai ser removida essa pasta.

Dessa forma estava já configurado com sucesso o storybook caso a app utilizasse o vite. Mas como utiliza rollup vai ser necessária adicionar duas libs para permitir a execução com sucesso:

yarn add vite @storybook/builder-vite --dev

No momento desse artigo gerou as seguintes versões:

"@storybook/builder-vite": "^8.6.12",
"vite": "^6.3.5"
Enter fullscreen mode Exit fullscreen mode

Modificação componentes

Antes de seguir com a documentação, vai ser adaptado os componentes da lib para definição das propriedades default e ajustes.
Começando pelo Tag.tsx:

  • Definição dos valores de props default e remoção de textWeight (pois já tem uma propriedade que permite mexer de forma customizável com o font weight do texto: textFontWeight):
// ...
const Tag = ({
  text = "Tag",
  type = "default",
  textColor = "#fff",
  textFontWeight = 600,
  textFontSize,
  textFontFamily,
  backgroundColor,
  format = "semiRounded",
  borderRadius,
  size = "medium",
  padding,
}: TagProps) => {
Enter fullscreen mode Exit fullscreen mode
  • Ajuste da definição de types do componente, uma vez que textWeight não é mais uma props presente no componente e que o format default agora é semiRounded:
// ...
export interface TagProps {
  type?: "default" | "success" | "alert" | "error";
  text: string;
  textColor?: string;
  textFontWeight?: number;
  textFontSize?: string;
  textFontFamily?: string;
  backgroundColor?: string;
  format?: "square" | "semiRounded" | "rounded";
  borderRadius?: string;
  size?: "small" | "medium" | "large";
  padding?: string;
}

export interface StyledTagProps {
  $type?: "default" | "success" | "alert" | "error";
  $textColor?: string;
  $textFontWeight?: number;
  $textFontSize?: string;
  $textFontFamily?: string;
  $backgroundColor?: string;
  $format?: "square" | "semiRounded" | "rounded";
  $borderRadius?: string;
  $size?: "small" | "medium" | "large";
  $padding?: string;
}
Enter fullscreen mode Exit fullscreen mode
  • Ajuste na definição de propriedades a partir do styled-components para levar em consideração de forma mais clara o size small, o type default e o format square:
// ...
export const StyledTag = styled.div<StyledTagProps>`
  border: none;
  padding: ${(props) =>
    props.$padding
      ? props.$padding
      : props.$size === "large"
        ? "15px 20px"
        : props.$size === "medium"
          ? "10px 12px"
          : props.$size === "small" && "7px"};
  background-color: ${(props) =>
    props.$backgroundColor
      ? props.$backgroundColor
      : props.$type === "error"
        ? "#e97451"
        : props.$type === "alert"
          ? "#f8de7e"
          : props.$type === "success"
            ? "#50c878"
            : props.$type === "default" && "#d3d3d3"};
  pointer-events: none;
  border-radius: ${(props) =>
    props.$borderRadius
      ? props.$borderRadius
      : props.$format === "rounded"
        ? "30px"
        : props.$format === "semiRounded"
          ? "5px"
          : props.$format === "square" && "0"};
  width: fit-content;
`;
Enter fullscreen mode Exit fullscreen mode
  • Ajuste na renderização do componente, definindo o font size do texto com base na props textFontSize ou size:
// ...
}: TagProps) => {
  const fontSize = textFontSize
    ? textFontSize
    : size === "large"
      ? "18px"
      : size === "small"
        ? "14px"
        : "16px";

  return (
    <StyledTag
      data-testid="tag"
      $type={type}
      $backgroundColor={backgroundColor}
      $format={format}
      $borderRadius={borderRadius}
      $size={size}
      $padding={padding}
    >
      <Text
        color={textColor}
        fontWeight={textFontWeight}
        fontSize={fontSize}
        fontFamily={textFontFamily}
      >
        {text}
      </Text>
    </StyledTag>
  );
};
Enter fullscreen mode Exit fullscreen mode

Como foi mexida nas propriedades default do componente de Tag, vai ser necessário mexer no arquivo de teste dele Tag.test.tsx:

  • Ajuste dos expect do primeiro teste para validar as propriedades default:
it("should render component with default properties", () => {
  render(<Tag text="Tag" />);

  const element = screen.getByTestId("tag");

  expect(element).toBeInTheDocument();
  expect(element).toHaveStyleRule("background-color", "#d3d3d3");
  expect(element).toHaveStyleRule("border-radius", "0");
  expect(element).toHaveStyleRule("padding", "7px");    
  expect(within(element).getByText("Tag")).toHaveStyleRule("color", "#fff");
});
Enter fullscreen mode Exit fullscreen mode
  • Remoção do teste should render component with bold text font weight, uma vez que a props que ele considerava foi removida.

Agora referente a Text.tsx:

  • Definição dos valores de props default:
// ...
const Text = ({
  children,
  color="#000",
  weight="normal",
  fontWeight,
  fontSize="16px",
  fontFamily,
}: TextProps) => (
Enter fullscreen mode Exit fullscreen mode
  • Ajuste na definição de propriedades a partir do styled-components, para em algumas situações só definir a propriedade se chegar a props relacionada:
// ...
export const StyledText = styled.span<StyledTextProps>`
  ${(props) => props.$color && `color: ${props.$color};`}
  ${(props) => props.$fontSize && `font-size: ${props.$fontSize};`}
  font-weight: ${(props) =>
    props.$fontWeight
      ? props.$fontWeight
      : props.$weight
        ? props.$weight
        : "normal"};
  ${(props) => props.$fontFamily && `font-family: ${props.$fontFamily};`}
`;
Enter fullscreen mode Exit fullscreen mode

Documentação dos componentes

Para a documentação dos componentes, vão ser criados dois tipos de arquivos para cada um deles:

  • stories: onde será definido os cenários de customização a serem mostrados na documentação
  • mdx: a documentação do componente, onde vão ser definidos os textos e incorporado os cenários presentes no arquivo de stories

Antes de partir para a documentação, será criado um componente responsável por centralizar os componentes renderizados na documentação e colocar um pequeno espaço entre eles, criando uma pasta StorybookContainer dentro de src/components:

  • arquivo StorybookContainer.tsx:
import React from "react";
import styled from "styled-components";

export interface StorybookContainerProps {
  children: React.ReactNode;
}

export const StyledStorybookContainer = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
`;

const StorybookContainer = ({ children }: StorybookContainerProps) => (
  <StyledStorybookContainer>
    {children}
  </StyledStorybookContainer>
);

export default StorybookContainer;
Enter fullscreen mode Exit fullscreen mode
  • arquivo index.ts:
export { default } from "./StorybookContainer";
Enter fullscreen mode Exit fullscreen mode

Agora, podemos começar com a documentação do componente de Tag, criando dentro da pasta src/components/Tag:

  • Tag.stories.tsx
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import Tag from "./Tag";
import StorybookContainer from "../StorybookContainer/StorybookContainer";

const meta: Meta<typeof Tag> = {
  title: "Tag",
  component: Tag,
  argTypes: {},
};

export default meta;

type Story = StoryObj<typeof Tag>;

export const Default: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
    </StorybookContainer>
  ),
};

export const PredefinedType: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} type="success" />
      <Tag {...args} type="alert" />
      <Tag {...args} type="error" />
    </StorybookContainer>
  ),
};

export const PredefinedFormat: Story = {
  args: {
    format: "square",
  },
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} format="semiRounded" />
      <Tag {...args} format="rounded" />
    </StorybookContainer>
  ),
};

export const PredefinedSize: Story = {
  args: {
    size: "small",
  },
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} size="medium" />
      <Tag {...args} size="large" />
    </StorybookContainer>
  ),
};

export const Text: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} text="Example" />
    </StorybookContainer>
  ),
};

export const BackgroundColor: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} backgroundColor="#3b82f6" />
    </StorybookContainer>
  ),
};

export const CustomFormat: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} borderRadius="15px 2px" />
    </StorybookContainer>
  ),
};

export const CustomSize: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} padding="20px 40px" />
    </StorybookContainer>
  ),
};

export const TextColor: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} textColor="#000" />
    </StorybookContainer>
  ),
};

export const TextFontWeight: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} textFontWeight={100} />
    </StorybookContainer>
  ),
};

export const TextFontFamily: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} textFontFamily="Arial" />
    </StorybookContainer>
  ),
};

export const TextFontSize: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Tag {...args} />
      <Tag {...args} textFontSize="20px" />
    </StorybookContainer>
  ),
};
Enter fullscreen mode Exit fullscreen mode

Nele em Meta é definido o componente que está sendo documentado e o título que vai aparecer no menu lateral do storybook. Em cada export const é definido um cenário que estará presente na documentação. Citando dois exemplos de cenário, o primeiro Default vai trazer o componente com as propriedades default dele, o segundo PredefinedType vai mostrar como o componente se comporta com a variação da props type.

  • Tag.mdx
import { Canvas, Controls, Meta } from "@storybook/blocks";
import * as Stories from "./Tag.stories";

<Meta of={Stories} />

# Tag

Tag base component.

<Canvas of={Stories.Default} withToolbar />

<Controls of={Stories.Default} />

## Predefined properties

### Type

There are four type predefined properties: default, success, alert and error.

<Canvas of={Stories.PredefinedType} withToolbar />

### Format

There are three format predefined properties: square, semiRounded (default) and rounded.

<Canvas of={Stories.PredefinedFormat} withToolbar />

### Size

There are three size predefined properties: small, medium (default) and large.

<Canvas of={Stories.PredefinedSize} withToolbar />

## Custom properties

### Text

Tag text can be modified.

<Canvas of={Stories.Text} withToolbar />

### Background Color

Tag background color can be modified.

<Canvas of={Stories.BackgroundColor} withToolbar />

### Format

Tag format can be modified by border radius definition.

<Canvas of={Stories.CustomFormat} withToolbar />

### Size

Tag size can be modified by padding definition.

<Canvas of={Stories.CustomSize} withToolbar />

### Text Color

Tag text color can be modified.

<Canvas of={Stories.TextColor} withToolbar />

### Text Font Weight

Tag text font weight can be modified.

<Canvas of={Stories.TextFontWeight} withToolbar />

### Text Font Family

Tag text font family can be modified.

<Canvas of={Stories.TextFontFamily} withToolbar />

### Text Font Size

Tag text font size can be modified.

<Canvas of={Stories.TextFontSize} withToolbar />
Enter fullscreen mode Exit fullscreen mode

No arquivo acima, em <Canvas of={Stories.Default} withToolbar /> é renderizado o componente com o cenário Default dele. Logo abaixo em <Controls of={Stories.Default} /> é renderizada uma tabela com todas as props do componente disponíveis para customização, permitindo que ao mexer nessas props seja possível ver em tempo real o impacto no componente acima da tabela.
Em Predefined properties são mostrados os cenários em que, com base nas props modificadas, a própria app define o css associado.
Em Custom properties são mostrados os cenários em que as props modificadas impactam diretamente no css definido.

Rodando localmente o storybook com yarn storybook:

Image description

Agora, será realizada a documentação do componente de Text, criando dentro da pasta src/components/Text:

  • Text.stories.tsx
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import Text from "./Text";
import StorybookContainer from "../StorybookContainer/StorybookContainer";

const meta: Meta<typeof Text> = {
  title: "Text",
  component: Text,
  argTypes: {},
};

export default meta;

type Story = StoryObj<typeof Text>;

export const Default: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
    </StorybookContainer>
  ),
};

export const PredefinedFontWeight: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} weight="bold">
        Text
      </Text>
    </StorybookContainer>
  ),
};

export const Color: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} color="#800080">
        Text
      </Text>
    </StorybookContainer>
  ),
};

export const CustomFontWeight: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} fontWeight={900}>
        Text
      </Text>
    </StorybookContainer>
  ),
};

export const FontSize: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} fontSize="30px">
        Text
      </Text>
    </StorybookContainer>
  ),
};

export const FontFamily: Story = {
  args: {},
  render: (args) => (
    <StorybookContainer>
      <Text {...args}>Text</Text>
      <Text {...args} fontFamily="Arial">
        Text
      </Text>
    </StorybookContainer>
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • Text.mdx
import { Canvas, Controls, Meta } from "@storybook/blocks";
import * as Stories from "./Text.stories";

<Meta of={Stories} />

# Text

Text base component.

<Canvas of={Stories.Default} withToolbar />

<Controls of={Stories.Default} />

## Predefined properties

### Font Weight

There are two font weight predefined properties: normal(default) and bold.

<Canvas of={Stories.PredefinedFontWeight} withToolbar />

## Custom properties

### Color

Text color can be modified.

<Canvas of={Stories.Color} withToolbar />

### Font Weight

Text font weight can be modified.

<Canvas of={Stories.CustomFontWeight} withToolbar />

### Font Size

Text font size can be modified.

<Canvas of={Stories.FontSize} withToolbar />

### Font Family

Text font family can be modified.

<Canvas of={Stories.FontFamily} withToolbar />
Enter fullscreen mode Exit fullscreen mode

Ficando da seguinte forma:

Image description

Deploy storybook

Agora temos os dois componentes documentados e rodando com sucesso o storybook localmente. Mas a ideia não é a documentação ser para uso de quem está desenvolvendo a lib, mas sim estar disponível para mostrar os componentes que a lib oferece para quem está pensando em adicionar ela. Para isso vai ser usado o chromatic para fazer o deploy da documentação e disponibilizar ela, que é um serviço de publicação criado pelos responsáveis do storybook.
Primeiro será adicionada a lib:

yarn add chromatic --dev

No momento desse artigo gerou a seguinte versão:

"chromatic": "^12.0.0",
Enter fullscreen mode Exit fullscreen mode

Agora será necessário seguir os seguintes passos para realizar o deploy:

  • Acessar página de cadastro
  • Selecionar Connect with GitHub para acessar (assumindo que seu repositório está lá)
  • Depois do cadastro, selecionar Choose GitHub repo
  • Selecionar o repositório da lib presente no seu github
  • Copiar e guardar o comando presente em Publish your Storybook, nesse ponto não dá para seguir sem executar no terminal o comando, mas antes vamos fazer o build do storybook
  • No terminal executar yarn build-storybook
  • Depois no terminal executar o comando passado na página do chromatic (toda vez que precisar fazer o deploy, esse exato comando precisará ser executado depois do build)

Por fim executando com sucesso, vai avisar no terminal que o storybook foi publicado com sucesso e passado a url onde é possível acessar ele em View your Storybook at "url".
Acessando a url vai ser possível ver a documentação dos componentes.

README.md

Agora que o storybook foi publicado, no README da app vai ser passada a url em que ele se encontra (aqui vou colocar a minha url gerada, no seu caso seria colocar a sua lá):

Image description

Arquivo .gitignore

No arquivo .gitignore será colocado para ignorar a pasta storybook-static, que é gerada quando se executa o yarn build-storybook:

dist
node_modules

*storybook.log
storybook-static
Enter fullscreen mode Exit fullscreen mode

package.json

Será mudada a versão dentro de package.json para 0.5.0, uma vez que uma nova versão da lib será disponibilizada:

{
  "name": "react-example-lib",
  "version": "0.5.0",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/griseduardo/react-example-lib.git"
  },
  "scripts": {
    "build": "rollup -c --bundleConfigAsCjs",
    "lint-src": "eslint src",
    "lint-src-fix": "eslint src --fix",
    "lint-fix": "eslint --fix",
    "format-src": "prettier src --check",
    "format-src-fix": "prettier src --write",
    "format-fix": "prettier --write",
    "test": "jest",
    "prepare": "husky",
    "storybook": "storybook dev -p 6006",
    "build-storybook": "storybook build"
  },
  "lint-staged": {
    "src/components/**/*.{ts,tsx}": [
      "yarn lint-fix",
      "yarn format-fix"
    ],
    "src/components/**/*.tsx": "yarn test --findRelatedTests --bail"
  },
  "devDependencies": {
    "@babel/core": "^7.26.10",
    "@babel/preset-env": "^7.26.9",
    "@babel/preset-react": "^7.26.3",
    "@babel/preset-typescript": "^7.26.0",
    "@eslint/js": "^9.19.0",
    "@rollup/plugin-commonjs": "^28.0.2",
    "@rollup/plugin-node-resolve": "^16.0.0",
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "11.1.6",
    "@storybook/addon-essentials": "^8.6.12",
    "@storybook/addon-interactions": "^8.6.12",
    "@storybook/addon-onboarding": "^8.6.12",
    "@storybook/blocks": "^8.6.12",
    "@storybook/builder-vite": "^8.6.12",
    "@storybook/react": "^8.6.12",
    "@storybook/react-vite": "^8.6.12",
    "@storybook/test": "^8.6.12",
    "@testing-library/dom": "^10.4.0",
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.2.0",
    "@types/jest": "^29.5.14",
    "@types/react": "^19.0.10",
    "@types/react-dom": "^19.0.4",
    "chromatic": "^12.0.0",
    "eslint": "^9.19.0",
    "eslint-plugin-react": "^7.37.4",
    "eslint-plugin-storybook": "^0.12.0",
    "husky": "^9.1.7",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "jest-styled-components": "^7.2.0",
    "lint-staged": "^15.5.0",
    "prettier": "^3.4.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "rollup": "^4.30.1",
    "rollup-plugin-dts": "^6.1.1",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "storybook": "^8.6.12",
    "styled-components": "^6.1.14",
    "typescript": "^5.7.3",
    "typescript-eslint": "^8.23.0",
    "vite": "^6.3.5"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "styled-components": "^6.1.14"
  },
  "eslintConfig": {
    "extends": [
      "plugin:storybook/recommended"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Arquivo CHANGELOG

Como uma nova versão será disponibilizada, será adicionado em CHANGELOG.md sobre o que foi modificado:

## 0.5.0

_May. 29, 2025_

- change Tag and Text default behavior
- add storybook
- add Tag and Text storybook docs

## 0.4.0

_Abr. 29, 2025_

- setup husky and lint-staged
- define pre-commit actions

## 0.3.0

_Mar. 24, 2025_

- setup jest and testing-library
- add components tests

## 0.2.0

_Fev. 24, 2025_

- setup typescript-eslint and prettier
- add custom rules

## 0.1.0

_Jan. 29, 2025_

- initial config
Enter fullscreen mode Exit fullscreen mode

Estrutura de pastas

A estrutura de pastas ficará da seguinte forma:

Image description

Publicação nova versão

Primeiro passo a ser realizado é ver se a execução do rollup ocorre com sucesso. Para isso será executado o yarn build no terminal, que foi definido em package.json.
Executando com sucesso, é realizar a publicação da nova versão da lib: npm publish --access public

Conclusão

A ideia desse artigo foi fazer o setup do storybook, criar a documentação dos componentes presentes na app e fazer o deploy dela, uma vez que a documentação é importante para mostrar os componentes que a lib está disponibilizando.
Segue o repositório no github e a lib no npmjs com as novas modificações.

Top comments (0)