Componentes e JSX

Componentes e JSX

Um componente é uma função que roda uma vez e devolve nós do DOM. As partes que mudam são as funções que você coloca no JSX.

O JSX cria nós de verdade

tsx
const button = <button class="btn">Salvar</button>;
button instanceof HTMLButtonElement; // true

Não há DOM virtual nem comparação de árvores. Quando o render() roda seu componente, os elementos são criados uma vez; depois, os signals atualizam os atributos e nós de texto específicos que os leem.

As três regras

1. Leia signals dentro de funções

tsx
function Greeting() {
  const [name, setName] = createSignal('Ada');

  return (
    <>
      <p>Olá, {name}</p>                         {/* atualiza */}
      <p>Olá, {() => name().toUpperCase()}</p>    {/* atualiza */}
      <p>Olá, {name()}</p>                       {/* lido uma vez: nunca atualiza */}
      <input value={name()} onInput={(e) => setName(e.currentTarget.value)} />
    </>
  );
}

Atributos funcionam do mesmo jeito: class={() => (active() ? 'on' : '')} é reativo, class={active() ? 'on' : ''} é calculado uma vez.

2. As props chegam como foram passadas

As props passam sem alteração. Uma função continua função, então callbacks e render props funcionam:

tsx
function Price(props: { amount: number; format: (n: number) => string }) {
  return <span>{props.format(props.amount)}</span>;
}

<Price amount={42} format={(n) => `R$ ${n.toFixed(2)}`} />;

Para aceitar um valor ou um signal, tipe a prop como MaybeAccessor<T> e leia com access():

tsx
import { access, type MaybeAccessor } from '@velozjs/dom';

function Badge(props: { count: MaybeAccessor<number> }) {
  return <span class="badge">{() => access(props.count)}</span>;
}

<Badge count={3} />
<Badge count={unread} /> {/* unread é o getter de um signal */}

3. Passe uma função para renderizar sob demanda

O JSX avalia os filhos antes de o componente pai rodar. Na maior parte do tempo tudo bem, mas quando o conteúdo deve ser criado depois ou dentro do escopo de um componente, passe uma função:

tsx
<Show when={open}>{() => <PainelPesado />}</Show>  {/* criado só quando aparece */}

<ErrorBoundary fallback={<p>Algo quebrou</p>}>
  {() => <Widget />}                                {/* erros são capturados */}
</ErrorBoundary>

<Theme.Provider value="dark">{() => <App />}</Theme.Provider> {/* enxerga o contexto */}

Eventos

Handlers são funções em props on*. Eventos que borbulham (click, input, keydown...) são delegados; os outros (focus, blur, mouseenter, scroll...) ganham um listener no próprio elemento. Nos dois casos, event.currentTarget é o elemento com o handler.

tsx
<button onClick={(e) => console.log(e.currentTarget)}>Clique</button>
<input onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} />

Refs

tsx
let input!: HTMLInputElement;
<input ref={(el) => (input = el)} />;

const box = { current: null as HTMLDivElement | null };
<div ref={box} />;

Estilos e classes

tsx
<div
  class={() => ['card', selected() && 'card-selected']}
  classList={() => ({ disabled: locked() })}
  style={() => ({ opacity: visible() ? 1 : 0.4, '--progress': `${pct()}%` })}
/>

Objetos de estilo aceitam camelCase e propriedades customizadas, números ganham px (menos propriedades sem unidade, como opacity), e chaves que somem na atualização seguinte são removidas.

Formulários

value, checked e selected são aplicados depois dos outros atributos e dos filhos, então <select value="b"> seleciona uma opção renderizada dentro dele, e o valor de um <input type="range"> não é cortado pela ordem em que você escreve min/max.

tsx
<select value={() => size()} onChange={(e) => setSize(e.currentTarget.value)}>
  <For each={sizes}>{(s) => <option value={s}>{s}</option>}</For>
</select>

Tipos

Cada elemento conhece seu tipo no DOM: o ref de um <dialog> recebe um HTMLDialogElement, e os handlers recebem um currentTarget tipado, então e.currentTarget.value dispensa conversão. Filhos que são funções de renderização (<Show>{(user) => …}</Show>) também são tipados. Exige TypeScript 5.1+.

SVG

Tags SVG (svg, path, circle, g, use...) são criadas no namespace SVG automaticamente, então ícones inline funcionam.

tsx
<svg viewBox="0 0 24 24" class="icon"><path d="M5 12h14" /></svg>

Renderizando

tsx
import { render } from '@velozjs/dom';

const dispose = render(() => <App />, document.getElementById('app')!);
dispose(); // remove o conteúdo e para todos os effects

Sem JSX

ts
import { createElement as h, render } from '@velozjs/dom';

render(() => h('button', { onClick: () => alert('oi') }, 'Olá'), document.body);