Web Components com Preact
Ricardo Mello
Posted on April 26, 2024
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>;
};
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
E se você está usando typescript, instale os types também:
npm i @types/preact-custom-element --save-dev
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');
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';
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>
Com isso, o web component será renderizado normalmente:
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
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>
Agora é só testar e ver um novo label e incrementando de dois em dois:
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']);
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>
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 🙂
Posted on April 26, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.