2.3 — Sintaxe de Templates vs JSX

Duas filosofias de como escrever markup em JavaScript.

Objetivos da Aula

  • Entender as diferenças entre JSX e templates Svelte
  • Comparar condicionais, loops e slots/children
  • Analisar prós e contras de cada abordagem

Filosofias Diferentes

Duas Abordagens

JSX (React)

"JavaScript com HTML dentro"

  • → Tudo e JavaScript, HTML e apenas sintaxe especial
  • → Condicionais: operadores ternarios, &&
  • → Loops: .map(), .filter()

Templates (Svelte)

"HTML com JavaScript dentro"

  • → HTML e a linguagem principal
  • → Condicionais: {#if}, {:else}
  • → Loops: {#each}

Interpolação Básica

React (JSX)

function Greeting({ name, age }) {
  const isAdult = age >= 18

  return (
    <div>
      <h1>Olá, {name}!</h1>
      <p>Você tem {age} anos.</p>
      <p>Status: {isAdult ? 'Adulto' : 'Menor'}</p>
      <p>Ano de nascimento: {new Date().getFullYear() - age}</p>
    </div>
  )
}

Svelte

<script>
  export let name
  export let age

  $: isAdult = age >= 18
</script>

<div>
  <h1>Olá, {name}!</h1>
  <p>Você tem {age} anos.</p>
  <p>Status: {isAdult ? 'Adulto' : 'Menor'}</p>
  <p>Ano de nascimento: {new Date().getFullYear() - age}</p>
</div>

Similaridade: Interpolação com {} é igual!


Condicionais

React: Ternários e &&

function UserStatus({ user, isLoading, error }) {
  // Condicional simples com &&
  return (
    <div>
      {isLoading && <Spinner />}

      {error && <p className="error">{error}</p>}

      {/* Ternário para if/else */}
      {user ? (
        <div>
          <h1>{user.name}</h1>
          <p>{user.email}</p>
        </div>
      ) : (
        <p>Faça login para continuar</p>
      )}

      {/* Múltiplas condições: ternários aninhados (feio!) */}
      {isLoading ? (
        <Spinner />
      ) : error ? (
        <Error message={error} />
      ) : user ? (
        <Profile user={user} />
      ) : (
        <LoginForm />
      )}
    </div>
  )
}

Svelte: Blocos Declarativos

<script>
  export let user
  export let isLoading
  export let error
</script>

<div>
  {#if isLoading}
    <Spinner />
  {/if}

  {#if error}
    <p class="error">{error}</p>
  {/if}

  {#if user}
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  {:else}
    <p>Faça login para continuar</p>
  {/if}

  <!-- Múltiplas condições: limpo e legível! -->
  {#if isLoading}
    <Spinner />
  {:else if error}
    <Error message={error} />
  {:else if user}
    <Profile {user} />
  {:else}
    <LoginForm />
  {/if}
</div>

Vantagem Svelte:

  • Sem ternários aninhados confusos
  • Sintaxe clara {#if}, {:else if}, {:else}, {/if}
  • Mais legível em condições complexas

Loops (Iteração)

React: .map()

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={todo.id}>
          <span>{index + 1}.</span>
          <span>{todo.text}</span>
          {todo.done && <span></span>}
        </li>
      ))}
    </ul>
  )
}

// Lista vazia
function EmptyAware({ items }) {
  return (
    <div>
      {items.length > 0 ? (
        <ul>
          {items.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ul>
      ) : (
        <p>Nenhum item encontrado</p>
      )}
    </div>
  )
}

Svelte: {#each}

<script>
  export let todos
</script>

<ul>
  {#each todos as todo, index (todo.id)}
    <li>
      <span>{index + 1}.</span>
      <span>{todo.text}</span>
      {#if todo.done}
        <span></span>
      {/if}
    </li>
  {/each}
</ul>

<!-- Lista vazia com {:else} -->
<div>
  {#each items as item (item.id)}
    <li>{item.name}</li>
  {:else}
    <p>Nenhum item encontrado</p>
  {/each}
</div>

Diferenças: | Aspecto | React | Svelte | |---------|-------|--------| | Sintaxe | .map() | {#each}...{/each} | | Chave | key={id} prop | (id) após variável | | Índice | Segundo param do map | as item, index | | Lista vazia | Ternário manual | {:else} built-in |


Desestruturação no Loop

React

function UserList({ users }) {
  return (
    <ul>
      {users.map(({ id, name, email, role }) => (
        <li key={id}>
          <strong>{name}</strong>
          <span>{email}</span>
          <span>({role})</span>
        </li>
      ))}
    </ul>
  )
}

Svelte

<ul>
  {#each users as { id, name, email, role } (id)}
    <li>
      <strong>{name}</strong>
      <span>{email}</span>
      <span>({role})</span>
    </li>
  {/each}
</ul>

Promises e Estados Async

React: Estado Manual

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    setLoading(true)
    setError(null)

    fetch(`/api/users/${userId}`)
      .then(r => {
        if (!r.ok) throw new Error('Erro ao carregar')
        return r.json()
      })
      .then(setUser)
      .catch(setError)
      .finally(() => setLoading(false))
  }, [userId])

  if (loading) return <Spinner />
  if (error) return <p>Erro: {error.message}</p>
  return <Profile user={user} />
}

Svelte: {#await}

<script>
  export let userId

  // Promise reativa
  $: userPromise = fetch(`/api/users/${userId}`).then(r => {
    if (!r.ok) throw new Error('Erro ao carregar')
    return r.json()
  })
</script>

{#await userPromise}
  <Spinner />
{:then user}
  <Profile {user} />
{:catch error}
  <p>Erro: {error.message}</p>
{/await}

<!-- Versão curta se não precisar de loading state -->
{#await userPromise then user}
  <Profile {user} />
{/await}

Vantagem Svelte: {#await} elimina todo o boilerplate de estados async!


Children vs Slots

React: children prop

// Card.jsx
function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">
        {children}
      </div>
    </div>
  )
}

// Uso
function App() {
  return (
    <Card title="Meu Card">
      <p>Conteúdo do card</p>
      <button>Ação</button>
    </Card>
  )
}

Svelte: slot

<!-- Card.svelte -->
<script>
  export let title
</script>

<div class="card">
  <h2>{title}</h2>
  <div class="card-body">
    <slot />
  </div>
</div>

<!-- Uso -->
<Card title="Meu Card">
  <p>Conteúdo do card</p>
  <button>Ação</button>
</Card>

Slots Nomeados

// React: props para diferentes "slots"
function Layout({ header, sidebar, children, footer }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{children}</main>
      <footer>{footer}</footer>
    </div>
  )
}

// Uso
<Layout
  header={<Nav />}
  sidebar={<Menu />}
  footer={<Copyright />}
>
  <p>Conteúdo principal</p>
</Layout>
<!-- Layout.svelte -->
<div class="layout">
  <header>
    <slot name="header" />
  </header>
  <aside>
    <slot name="sidebar" />
  </aside>
  <main>
    <slot />
  </main>
  <footer>
    <slot name="footer" />
  </footer>
</div>

<!-- Uso -->
<Layout>
  <Nav slot="header" />
  <Menu slot="sidebar" />
  <p>Conteúdo principal</p>
  <Copyright slot="footer" />
</Layout>

Slot Fallback (Conteúdo Padrão)

<!-- Button.svelte -->
<button>
  <slot>Clique aqui</slot>  <!-- Fallback -->
</button>

<!-- Uso -->
<Button />              <!-- Mostra: "Clique aqui" -->
<Button>Enviar</Button> <!-- Mostra: "Enviar" -->

Atributos e Props

React

function Button({ variant, disabled, onClick, className, ...rest }) {
  return (
    <button
      className={`btn btn-${variant} ${className || ''}`}
      disabled={disabled}
      onClick={onClick}
      {...rest}  // Spread de props restantes
    >
      Click
    </button>
  )
}

// Uso
<Button
  variant="primary"
  disabled={false}
  onClick={handleClick}
  data-testid="submit-btn"
/>

Svelte

<script>
  export let variant = 'default'
  export let disabled = false
</script>

<button
  class="btn btn-{variant}"
  {disabled}
  on:click
  {...$$restProps}
>
  <slot>Click</slot>
</button>

<!-- Uso -->
<Button
  variant="primary"
  disabled={false}
  on:click={handleClick}
  data-testid="submit-btn"
/>

Shorthands do Svelte:

  • {disabled} = disabled={disabled}
  • on:click = encaminha evento para cima
  • {...$$restProps} = props não declaradas

Classes Condicionais

React

// Concatenação manual
<div className={`card ${isActive ? 'active' : ''} ${isHighlighted ? 'highlighted' : ''}`}>

// Com biblioteca (clsx ou classnames)
import clsx from 'clsx'
<div className={clsx('card', { active: isActive, highlighted: isHighlighted })}>

Svelte

<!-- Diretiva class: nativa -->
<div
  class="card"
  class:active={isActive}
  class:highlighted={isHighlighted}
>

<!-- Shorthand quando nome é igual à variável -->
<div class="card" class:active class:highlighted>

<!-- Também funciona concatenação -->
<div class="card {isActive ? 'active' : ''}">

Estilos Inline

React

// Objeto de estilos (camelCase!)
<div style={{
  backgroundColor: color,
  fontSize: `${size}px`,
  marginTop: '20px'
}}>

Svelte

<!-- String (como HTML normal) -->
<div style="background-color: {color}; font-size: {size}px; margin-top: 20px">

<!-- Ou diretiva style: (Svelte 3.46+) -->
<div
  style:background-color={color}
  style:font-size="{size}px"
  style:margin-top="20px"
>

HTML Dinâmico (Perigoso!)

React

// dangerouslySetInnerHTML (nome assustador de propósito!)
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />

Svelte

<!-- @html (mais curto, igualmente perigoso) -->
<div>{@html htmlContent}</div>

⚠️ Aviso: Ambos são vulneráveis a XSS! Sempre sanitize o HTML!


Tabela Comparativa

FuncionalidadeReact JSXSvelte Template
Interpolação{value}{value}
If simples{condition && <El />}{#if condition}<El />{/if}
If/else{cond ? <A /> : <B />}{#if cond}<A />{:else}<B />{/if}
Loop{arr.map(x => <El key={x.id} />)}{#each arr as x (x.id)}<El />{/each}
Loop vazioTernário manual{#each}...{:else}...{/each}
AsyncuseState + useEffect{#await promise}...{/await}
Children{children}<slot />
Named slotsProps separadas<slot name="x" />
Class condicionalclassName={cond ? 'x' : ''}class:x={cond}
Style dinâmicostyle={{ prop: val }}style:prop={val}
HTML rawdangerouslySetInnerHTML{@html content}
Spread props{...props}{...$$restProps}

✅ Desafio da Aula

Objetivo

Converter um componente React complexo com múltiplas features de template para Svelte.

Componente React

function ProductList({ products, loading, error, onAddToCart }) {
  if (loading) return <div className="spinner">Carregando...</div>
  if (error) return <div className="error">{error}</div>

  return (
    <div className="product-list">
      {products.length > 0 ? (
        <ul>
          {products.map(product => (
            <li
              key={product.id}
              className={`product ${product.inStock ? '' : 'out-of-stock'}`}
            >
              <img src={product.image} alt={product.name} />
              <h3>{product.name}</h3>
              <p className="price">R$ {product.price.toFixed(2)}</p>
              {product.discount > 0 && (
                <span className="discount">-{product.discount}%</span>
              )}
              <button
                disabled={!product.inStock}
                onClick={() => onAddToCart(product)}
              >
                {product.inStock ? 'Adicionar' : 'Indisponível'}
              </button>
            </li>
          ))}
        </ul>
      ) : (
        <p className="empty">Nenhum produto encontrado</p>
      )}
    </div>
  )
}

Spec de Verificação

  • Mostra loading state
  • Mostra erro se houver
  • Lista produtos com loop
  • Mostra mensagem se lista vazia
  • Classes condicionais funcionam
  • Desconto aparece só quando > 0
  • Botão desabilitado quando sem estoque

Solução

🔍 Clique para ver a solução
<script>
  import { createEventDispatcher } from 'svelte'

  export let products = []
  export let loading = false
  export let error = null

  const dispatch = createEventDispatcher()

  function addToCart(product) {
    dispatch('addToCart', product)
  }
</script>

{#if loading}
  <div class="spinner">Carregando...</div>
{:else if error}
  <div class="error">{error}</div>
{:else}
  <div class="product-list">
    {#each products as product (product.id)}
      <li class="product" class:out-of-stock={!product.inStock}>
        <img src={product.image} alt={product.name} />
        <h3>{product.name}</h3>
        <p class="price">R$ {product.price.toFixed(2)}</p>

        {#if product.discount > 0}
          <span class="discount">-{product.discount}%</span>
        {/if}

        <button
          disabled={!product.inStock}
          on:click={() => addToCart(product)}
        >
          {product.inStock ? 'Adicionar' : 'Indisponível'}
        </button>
      </li>
    {:else}
      <p class="empty">Nenhum produto encontrado</p>
    {/each}
  </div>
{/if}

<style>
  .out-of-stock { opacity: 0.5; }
  .discount { color: red; }
</style>

Próxima aula: 2.4 — Gerenciamento de Estado: Stores vs Context/Redux