Web Components com Preact

ricmello

Ricardo Mello

Posted on April 26, 2024

Web Components com Preact

O Preact se intitula como "Fast 3kB alternative to React with the same modern API". E na minha opinião, ele tá se subestimando. Não é minha intenção entrar em uma comparação de React vs Preact, mas a performance dele a possibilidade de criar web components são duas grandes vantagens. É esse segundo ponto que vamos abordar hoje.

Eu escolhi o Preact ao invés de outras libs como Lit ou Stencil porque além de ele ser muito leve e possuir uma ótima performance, os componentes são iguais aos do React, o que significa uma curva de aprendizado muito pequena caso o seu time já conheça React. Você consegue criar uma lib agnóstica sem abrir mão do TSX/JSX, hooks, e tudo aquilo que faz o React ser o que ele é, o que na minha opinião faz o Preact ser o melhor dos dois mundos no desenvolvimento de bibliotecas.

Por último, mas não menos importante, o Preact já oferece suporte nativo aos Signals, que te permite criar um gerenciamento de estado atômico e, mais uma vez, muito performático.


O projeto

O nosso objetivo vai ser bem simples, que é transformar um componente de counter em um web component. Sabe aquele counter que aparece quando você gera uma aplicação com o vite? Ele mesmo. Vamos usar uma técnica simples pra transformar esse cara em um web component que pode ser utilizado em qualquer lugar, inclusive em um HTML comum.

Mão na massa

Pra esse exemplo eu criei um projeto Preact no Stackblitz e vou alterá-lo para exportar e renderizar os web components. Se preferir, você também pode criar um projeto local usando o getting started.

Eu deletei o app.tsx e o app.css. Também removi o render() do main.tsx. Como nós vamos exportar o web component pra ser usado diretamente no HTML, não faz sentido renderizar o preact como aplicação.

Pra começar, crie um componente counter.tsx com o seguinte conteúdo:

import { FunctionalComponent } from 'preact';
import { useCallback, useState } from 'preact/hooks';

export const Counter: FunctionalComponent = () => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  return <button onClick={increment}>count is {count}</button>;
};
Enter fullscreen mode Exit fullscreen mode

O pulo do gato

Qualquer componente preact pode ser registrado como web component usando o mini wrapper preact-custom-element. Esse cara transforma o componente em Web Component seguindo a V1 da especificação Custom Elements.

Pra instalar o wrapper, rode o seguinte comando:

npm install preact-custom-element
Enter fullscreen mode Exit fullscreen mode

E se você está usando typescript, instale os types também:

npm i @types/preact-custom-element --save-dev
Enter fullscreen mode Exit fullscreen mode

Agora, é só usar o wrapper no componente e o código do seu counter deve ficar assim:

import { FunctionalComponent } from 'preact';
import { useCallback, useState } from 'preact/hooks';
import register from 'preact-custom-element';

export const Counter: FunctionalComponent = () => {...};

register(Counter, 'ric-counter');
Enter fullscreen mode Exit fullscreen mode

Por último, exporte o componente no main.tsx. Como removemos a maior parte desse arquivo, ele deve ficar assim:

import './index.css';

export * from './counter';
Enter fullscreen mode Exit fullscreen mode

A partir daí você já consegue utilizar o seu web component no index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Preact + TS + Web Components</title>
  </head>
  <body>
    <ric-counter></ric-counter>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Com isso, o web component será renderizado normalmente:

Botão counter sendo renderizado como web component

Você deve ter notado que eu declarei a tag <ric-counter></ric-counter> antes do import do main.tsx. Isso é pra mostrar que ele consegue detectar tags que já foram declaradas antes de ele ser inicializado, o que significa que você não precisa se preocupar em carregar o js antes das tags.

Atributos

Os atributos são essenciais pra permitir que a aplicação possa controlar o comportamento dos componentes, ao mesmo tempo que nós como desenvolvedores de componentes definimos o que pode ser controlado.

Os web components recebem os atributos como string por padrão, então é importante ter em mente que os seus dados podem precisar ser convertidos antes da utilização. Eu considero uma boa prática fazer isso já nas primeiras linhas do componente, e trabalhar internamente com a variável no tipo que eu quiser pra sempre converter tudo em um lugar único:

import { useCallback, useState } from 'preact/hooks';
import register from 'preact-custom-element';
import { FunctionalComponent } from 'preact';

interface CounterProps {
  label?: string;
  steps?: string;
}

export const Counter: FunctionalComponent<CounterProps> = ({
  label = 'Count is',
  steps: rawSteps = '1',
}) => {
  // Converto o atributo steps (rawSteps) pra number e uso como steps internamente
  const steps = Number(rawSteps);
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(count + steps);
  }, [count]);

  return (
    <button onClick={increment}>
      {label} {count}
    </button>
  );
};

register(Counter, 'ric-counter', ['label', 'steps'], { shadow: false });
//          ^            ^           ^                       ^
//          |         tag HTML       |                   shadow-dom
//     Componente           Atributos observados

Enter fullscreen mode Exit fullscreen mode

Sempre que um atributo observado for alterado, o componente vai ser atualizado. Os atributos que não estiverem nessa lista só serão detectados caso sejam passados na inicialização.

Já o shadow, é um booleano que indica se o seu componente deve usar ou não o Shadow DOM.

O HTML com os atributos:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Preact + TS + Web Components</title>
  </head>
  <body>
    <ric-counter label="Custom count:" steps="2"></ric-counter>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Agora é só testar e ver um novo label e incrementando de dois em dois:

Botão counter customizado

Callbacks

Os callbacks são um tipo importante de atributo, afinal as pessoas que vão utilizar o nosso componente precisam saber se um botão foi clicado e/ou se o estado do componente mudou.

Como os atributos de um web component são passados como strings, é preciso passar as funções de callback de uma forma diferente: via JavaScript.

E pra tornar isso possível vamos adicionar uma nova propriedade ao nosso componente chamada onChange e executar esse callback dentro a função de increment:

import { useCallback, useState } from 'preact/hooks';
import register from 'preact-custom-element';
import { FunctionalComponent } from 'preact';

interface CounterProps {
  label?: string;
  steps?: string;
  onChange?: (count: number) => void;
}

export const Counter: FunctionalComponent<CounterProps> = ({
  label = 'Count is',
  steps: rawSteps = '1',
  onChange,
}) => {
  const steps = Number(rawSteps);
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    setCount(count + steps);
    onChange && onChange(count + steps);
  }, [count, onChange]);

  return (
    <button onClick={increment}>
      {label} {count}
    </button>
  );
};

register(Counter, 'ric-counter', ['label', 'steps', 'onChange']);
Enter fullscreen mode Exit fullscreen mode

E agora no HTML, basta usar o querySelector e atribuir a propriedade com a nossa função de callback:

<body>
  <ric-counter label="Custom count:" steps="2"></ric-counter>
  <script type="module" src="/src/main.tsx"></script>
  <script type="module">
    const counter = document.querySelector('ric-counter');
    counter.onChange = () => {
      console.log('counter changed');
    };
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

Agora é só testar e ver um console.log sendo executado cada vez que clicar no botão. Você também pode ver o resultado final no meu Stackblitz.

Conclusão

Poder utilizar um web component direto no HTML abre portas pra muitos casos reais de uso. Você pode exportar os seus componentes pra serem incluídos em sites wordpress, ou até mesmo criar wrappers pra outros frameworks e reutilizar o seu código em diferentes interfaces.

Como eu disse antes nesse post, eu não acho que valha a pena desenvolver aplicações inteiras usando web components podendo usar direto o preact ou outro framework, mas se o seu contexto é libs, widgets, ou qualquer tipo de componente que é incorporado em outro website (por exemplo um chat), essa abordagem vai fazer muito sentido.


Se você curte o assunto e gostaria de ver mais posts desses por aqui, me avisa porque é algo que eu tenho gostado bastante de estudar, e acho que realmente vale a pena o investimento.

E se você não gostou de alguma coisa, ou tem alguma sugestão que possa fazer esse e os próximos artigos menores, lança aqui nos comentários também. Todo comentário é bem vindo e as críticas vão me ajudar a aprimorar os próximos posts 🙂

💖 💪 🙅 🚩
ricmello
Ricardo Mello

Posted on April 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024

How to Use KitOps with MLflow
beginners How to Use KitOps with MLflow

November 29, 2024

Modern C++ for LeetCode 🧑‍💻🚀
leetcode Modern C++ for LeetCode 🧑‍💻🚀

November 29, 2024