# Um contador entre React e Vue

Conecte as duas apps do tutorial pelo store do WU, com arquivos completos, permissões, inscrições e um teste de desmontagem.

<span id="lo-que-vas-a-construir"></span>

<span class="section-anchor" id="section-1"></span>

## O que você vai construir

Continue em `mi-plataforma`, o mesmo workspace de [Adicionar uma segunda app](/pt/docs/guides/add-an-app/). Você substituirá os contadores locais por um contador compartilhado: **somar no React também muda o Vue, e somar no Vue também muda o React**.

A página Astro `/contador/` mostrará as duas apps juntas. Elas continuarão com seus próprios componentes e renderizadores. O dado comum ficará em `wu.store`, no caminho `demo.counter`.

Você precisa do Node.js 24 e do workspace criado com WU CLI 1.2.0, com `mi-plataforma` em React e `inspector` em Vue. O código usa as APIs publicadas de `wu-framework` 4.0.0 e as dependências que você já instalou com `wu install`.

<span id="1-aplicar-los-archivos-al-mismo-proyecto"></span>

<span class="section-anchor" id="section-2"></span>

## 1. Aplicar os arquivos ao mesmo projeto

Pare o servidor com **Ctrl+C**. [Baixe os arquivos completos do contador](/examples/wu-shared-counter.zip) e copie seu conteúdo **para a raiz de `mi-plataforma/`**, ao lado do seu `wu.config.json`.

O ZIP contém arquivos para incorporar ao workspace do tutorial; não é instalado nem executado separadamente. Substitua os arquivos de mesmo nome e mantenha as outras pastas do projeto. Se já personalizou esses arquivos, salve suas alterações antes de substituí-los.

| Arquivo | Responsabilidade |
| --- | --- |
| `mf-mi-plataforma/src/App.tsx` | Mostrar e atualizar o dado pelo React. |
| `mf-mi-plataforma/src/main.tsx` | Conectar a montagem e a desmontagem do React ao WU. |
| `mf-inspector/src/App.vue` | Mostrar e atualizar o mesmo dado pelo Vue. |
| `mf-inspector/src/main.ts` | Conectar a montagem e a desmontagem do Vue ao WU. |
| `wu.json` dentro de cada app | Solicitar acesso apenas a `demo.counter`. |
| `vite.config.js` dentro de cada app | Levar o manifesto fonte ao build dessa app. |
| `wu.config.json` | Manter as duas apps e conceder permissões ao usar o shell gerado. |
| `shell/src/pages/contador.astro` | Montar ambas as apps, conceder permissões e inicializar o dado. |
| `shell/astro.config.mjs` | Emitir o CSS do shell em arquivos que `wu serve` pode servir. |

**Se já adicionou mais apps ao workspace**, mantenha seu `wu.config.json` e adicione `"grants": ["store.read:demo.counter", "store.write:demo.counter"]` apenas às entradas existentes `mi-plataforma` e `inspector`. O arquivo do ZIP corresponde ao tutorial de duas apps.

Você também pode copiar manualmente os onze arquivos completos apresentados abaixo. Não precisa adicionar uma biblioteca de componentes nem instalar novos pacotes.

<span id="2-construir-y-ejecutar"></span>

<span class="section-anchor" id="section-3"></span>

## 2. Fazer o build e executar

Na raiz do workspace:

```sh
npm --prefix shell run build
wu dev --native --port 3000 --no-open
```

Espere `npm --prefix shell run build` terminar sem erros. Ele prepara a nova página Astro antes de iniciar, pois um build existente do shell pode ser reutilizado. Enquanto `wu dev --native` está ativo, a CLI 1.2.0 observa as fontes do shell, recompila e recarrega a página quando mudam. Você pode continuar editando sem parar o servidor; confira o terminal se o Astro informar um erro.

Abra [http://127.0.0.1:3000/contador/](http://127.0.0.1:3000/contador/). Você deve ver **Un contador, React y Vue**, os dois cartões e um zero em cada um.

1. Clique em **Sumar desde React**: ambos os valores passam de **0 para 1**.
2. Clique em **Sumar desde Vue**: ambos passam de **1 para 2**.
3. Clique em **Desmontar Vue**: o cartão dele desaparece e React mantém o **2**.
4. Clique em **Sumar desde React**: React mostra **3**.
5. Clique em **Montar Vue**: Vue lê o dado atual e também mostra **3**.
6. Clique em **Restar desde Vue**: ambas as apps mostram **2**.

Desmontar a app remove sua interface e sua inscrição; **não apaga o dado compartilhado**. Recarregar a página inteira inicia uma nova sessão com zero: este exemplo guarda o dado na memória, sem persistência no navegador ou em um servidor.

<span id="3-cómo-llega-un-cambio-a-ambas-apps"></span>

<span class="section-anchor" id="section-4"></span>

## 3. Como uma alteração chega às duas apps

O shell inicializa `demo.counter` uma única vez. Cada componente lê seu valor inicial e se inscreve com `store.on`. Um botão chama `store.update`; o WU grava o novo valor e notifica os dois componentes. React atualiza seu `useState` e Vue atualiza seu `ref`.

`update` calcula o novo valor a partir do dado atual do store. Assim, os botões não dependem de a última notificação já ter sido desenhada na tela. As notificações chegam em uma microtask; `on` não entrega automaticamente o valor inicial.

As apps usam **`context.wu`**, a fachada que o WU entrega durante a montagem. `waitForWu` localiza o runtime do shell para registrar o ciclo de vida com `wu.define`; as leituras e escritas do componente passam pela fachada da própria app.

<span id="el-componente-react"></span>

<span class="section-anchor" id="section-5"></span>

### O componente React

**`mf-mi-plataforma/src/App.tsx`** · [Abrir arquivo completo](/examples/browser/shared-counter/mf-mi-plataforma/src/App.tsx.txt)

```tsx
import { useEffect, useState } from 'react';
import type { WuAppFacade } from 'wu-framework';

export default function App({ appWu }: { appWu: WuAppFacade }) {
  const [count, setCount] = useState(() => appWu.store.get('demo.counter') ?? 0);

  useEffect(() => {
    const stop = appWu.store.on('demo.counter', (value) => setCount(value));
    setCount(appWu.store.get('demo.counter') ?? 0);
    return stop;
  }, [appWu]);

  function change(delta: number) {
    appWu.store.update('demo.counter', (current) => (current ?? 0) + delta);
  }

  return (
    <section aria-label="Contador React">
      <h2>React · mi-plataforma</h2>
      <p>Valor compartido: <output aria-live="polite">{count}</output></p>
      <button type="button" onClick={() => change(-1)}>Restar desde React</button>{' '}
      <button type="button" onClick={() => change(1)}>Sumar desde React</button>
    </section>
  );
}
```

O efeito se inscreve e lê o dado novamente para cobrir o intervalo entre a primeira renderização e a conexão. Seu `return stop` libera a inscrição quando React desmonta o componente.

**`mf-mi-plataforma/src/main.tsx`** · [Abrir arquivo completo](/examples/browser/shared-counter/mf-mi-plataforma/src/main.tsx.txt)

```tsx
import React from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { waitForWu } from 'wu-framework/adapters/shared';
import App from './App.tsx';

const wu = await waitForWu();
let root: Root | undefined;

wu.define('mi-plataforma', {
  mount(container, context) {
    if (!context?.wu) throw new Error('Esta app necesita el contexto de WU.');
    root = createRoot(container);
    root.render(<App appWu={context.wu} />);
  },
  unmount() {
    root?.unmount();
    root = undefined;
  },
});
```

O ciclo de vida cria a raiz do React e passa `context.wu` como prop. Ao desmontar a microapp, `root.unmount()` executa a limpeza do efeito.

<span id="el-componente-vue"></span>

<span class="section-anchor" id="section-6"></span>

### O componente Vue

**`mf-inspector/src/App.vue`** · [Abrir arquivo completo](/examples/browser/shared-counter/mf-inspector/src/App.vue.txt)

```vue
<script setup lang="ts">
import { onUnmounted, ref } from 'vue';
import type { WuAppFacade } from 'wu-framework';

const { appWu } = defineProps<{ appWu: WuAppFacade }>();
const count = ref(appWu.store.get('demo.counter') ?? 0);
const stop = appWu.store.on('demo.counter', (value) => { count.value = value; });
onUnmounted(stop);

function change(delta: number) {
  appWu.store.update('demo.counter', (current) => (current ?? 0) + delta);
}
</script>

<template>
  <section aria-label="Contador Vue">
    <h2>Vue · inspector</h2>
    <p>Valor compartido: <output aria-live="polite">{{ count }}</output></p>
    <button type="button" @click="change(-1)">Restar desde Vue</button>
    {{ ' ' }}
    <button type="button" @click="change(1)">Sumar desde Vue</button>
  </section>
</template>
```

Vue mantém um `ref` para desenhar o dado. `onUnmounted(stop)` libera a inscrição quando o componente é removido.

**`mf-inspector/src/main.ts`** · [Abrir arquivo completo](/examples/browser/shared-counter/mf-inspector/src/main.ts.txt)

```ts
import { createApp, type App as VueApp } from 'vue';
import { waitForWu } from 'wu-framework/adapters/shared';
import App from './App.vue';

const wu = await waitForWu();
let app: VueApp | undefined;

wu.define('inspector', {
  mount(container, context) {
    if (!context?.wu) throw new Error('Esta app necesita el contexto de WU.');
    app = createApp(App, { appWu: context.wu });
    app.mount(container);
  },
  unmount() {
    app?.unmount();
    app = undefined;
  },
});
```

`app.unmount()` executa a limpeza do Vue. Ao montar novamente, uma nova instância é criada e o valor atual do WU é lido; o componente não reinicia o contador.

<span id="4-qué-permisos-se-conceden"></span>

<span class="section-anchor" id="section-7"></span>

## 4. Quais permissões são concedidas

Cada manifesto solicita `store.read` e `store.write` apenas para `demo.counter`. A leitura permite consultar e se inscrever; `update` precisa tanto de leitura quanto de escrita. O shell concede esses mesmos recursos por `grants`.

Uma app não ganha acesso ao store inteiro por conhecer um caminho. O WU calcula as permissões efetivas a partir do que a app solicitou e do que o host concedeu. `required: true` faz uma solicitação sem cobertura impedir o registro da app.

**`mf-mi-plataforma/wu.json`** · [Abrir arquivo completo](/examples/browser/shared-counter/mf-mi-plataforma/wu.json.txt)

```json
{
  "name": "mi-plataforma",
  "version": "1.0.0",
  "entry": "src/main.tsx",
  "styleMode": "shared",
  "wu": {
    "permissions": [
      { "capability": "store.read", "resource": "demo.counter", "required": true },
      { "capability": "store.write", "resource": "demo.counter", "required": true }
    ]
  }
}
```

**`mf-inspector/wu.json`** · [Abrir arquivo completo](/examples/browser/shared-counter/mf-inspector/wu.json.txt)

```json
{
  "name": "inspector",
  "version": "1.0.0",
  "entry": "src/main.ts",
  "styleMode": "shared",
  "wu": {
    "permissions": [
      { "capability": "store.read", "resource": "demo.counter", "required": true },
      { "capability": "store.write", "resource": "demo.counter", "required": true }
    ]
  }
}
```

<span id="conservar-el-manifest-al-compilar"></span>

<span class="section-anchor" id="section-8"></span>

### Manter o manifesto ao compilar

No desenvolvimento nativo, a CLI serve o `wu.json` da raiz de cada microapp. Cada configuração Vite adiciona um pequeno plugin que grava esse mesmo arquivo em seu `dist/`. Depois, `wu build` mantém suas permissões e atualiza `entry` para apontar para o JavaScript compilado. Você não mantém uma segunda cópia do manifesto manualmente.

**`mf-mi-plataforma/vite.config.js`** · [Abrir arquivo completo](/examples/browser/shared-counter/mf-mi-plataforma/vite.config.js.txt)

```js
import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react(),
    {
      name: 'emit-wu-manifest',
      generateBundle() {
        this.emitFile({
          type: 'asset',
          fileName: 'wu.json',
          source: readFileSync(new URL('./wu.json', import.meta.url), 'utf8'),
        });
      },
    },
  ],
  build: { target: 'esnext' },
});
```

**`mf-inspector/vite.config.js`** · [Abrir arquivo completo](/examples/browser/shared-counter/mf-inspector/vite.config.js.txt)

```js
import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue(),
    {
      name: 'emit-wu-manifest',
      generateBundle() {
        this.emitFile({
          type: 'asset',
          fileName: 'wu.json',
          source: readFileSync(new URL('./wu.json', import.meta.url), 'utf8'),
        });
      },
    },
  ],
  build: { target: 'esnext' },
});
```

<span id="mantener-el-shell-generado"></span>

<span class="section-anchor" id="section-9"></span>

### Manter o shell gerado

Esta configuração mantém as duas apps do tutorial e adiciona suas concessões. Assim, você também pode abri-las pela navegação da página principal do workspace. Essa navegação mostra uma app por vez; `/contador/` permite ver as duas simultaneamente.

**`wu.config.json`** · [Abrir arquivo completo](/examples/browser/shared-counter/wu.config.json.txt)

```json
{
  "name": "mi-plataforma",
  "version": "1.1.0",
  "shell": {
    "dir": "shell", "port": 4321, "framework": "astro",
    "dev_cmd": "npx astro dev", "build_cmd": "npx astro build"
  },
  "apps": [
    {
      "name": "mi-plataforma", "dir": "mf-mi-plataforma", "framework": "react", "port": 5001,
      "grants": ["store.read:demo.counter", "store.write:demo.counter"]
    },
    {
      "name": "inspector", "dir": "mf-inspector", "framework": "vue", "port": 5002,
      "grants": ["store.read:demo.counter", "store.write:demo.counter"]
    }
  ],
  "proxy": { "port": 3000, "open_browser": true }
}
```

<span id="5-la-página-astro-que-las-conecta"></span>

<span class="section-anchor" id="section-10"></span>

## 5. A página Astro que as conecta

Esta é a página completa. Seu script é executado no navegador, inicializa o WU e monta cada app no respectivo contêiner. Declara explicitamente as concessões desta página porque ela é um host adicional ao shell gerado.

A inicialização do dado fica no host e é executada antes da montagem. O botão do Vue usa `unmount` com `force: true` para aguardar a limpeza imediata antes de permitir outra montagem.

**`shell/src/pages/contador.astro`** · [Abrir arquivo completo](/examples/browser/shared-counter/shell/src/pages/contador.astro.txt)

```astro
<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Un contador compartido · WU</title>
    <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
  </head>
  <body>
    <main>
      <a href="/">Volver al workspace</a>
      <h1>Un contador, React y Vue</h1>
      <p>Las dos apps leen y escriben el mismo dato en WU.</p>
      <p id="estado" role="status">Cargando las apps…</p>
      <div class="contadores">
        <div id="react-counter"></div>
        <div id="vue-counter"></div>
      </div>
      <button id="toggle-vue" type="button" disabled>Desmontar Vue</button>
      <p>Desmonta Vue, cambia el valor desde React y vuelve a montar Vue.</p>
    </main>
    <script>
      import { wu } from 'wu-framework';

      const status = document.querySelector<HTMLElement>('#estado')!;
      const toggle = document.querySelector<HTMLButtonElement>('#toggle-vue')!;
      let vueMounted = false;

      try {
        await wu.init({
          sandbox: 'module',
          apps: [
            {
              name: 'mi-plataforma', url: '/mf-mi-plataforma',
              grants: ['store.read:demo.counter', 'store.write:demo.counter'],
            },
            {
              name: 'inspector', url: '/mf-inspector',
              grants: ['store.read:demo.counter', 'store.write:demo.counter'],
            },
          ],
        });
        if (wu.store.get('demo.counter') === undefined) wu.store.set('demo.counter', 0);
        await wu.mount('mi-plataforma', '#react-counter');
        await wu.mount('inspector', '#vue-counter');
        vueMounted = true;
        toggle.disabled = false;
        status.textContent = 'React y Vue conectados.';
      } catch (error) {
        status.textContent = `No se pudo iniciar: ${error}`;
      }

      toggle.addEventListener('click', async () => {
        toggle.disabled = true;
        try {
          if (vueMounted) {
            await wu.unmount('inspector', { force: true });
          } else {
            await wu.mount('inspector', '#vue-counter');
          }
          vueMounted = !vueMounted;
          toggle.textContent = vueMounted ? 'Desmontar Vue' : 'Montar Vue';
          status.textContent = vueMounted ? 'React y Vue conectados.' : 'Vue desmontado. React sigue conectado.';
        } catch (error) {
          status.textContent = `No se pudo cambiar Vue: ${error}`;
        } finally {
          toggle.disabled = false;
        }
      });
    </script>
    <style is:global>
      body { margin: 0; font: 1rem/1.6 system-ui, sans-serif; color: #192a3b; background: #f5f7fb; }
      main { max-width: 64rem; margin: auto; padding: 2rem 1rem; }
      a { color: #124ca6; }
      .contadores { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr)); gap: 1rem; margin: 1.5rem 0; }
      section { padding: 1.5rem; border: 1px solid #bac7d6; border-radius: 1rem; background: white; }
      h2 { margin-top: 0; font-size: 1.25rem; }
      output { font-size: 2rem; font-weight: 700; }
      button { font: inherit; padding: .6rem .9rem; margin-block: .25rem; border: 1px solid #617387; border-radius: .4rem; background: white; color: #192a3b; cursor: pointer; }
      button:focus-visible, a:focus-visible { outline: 3px solid #155fc9; outline-offset: 3px; }
      button:disabled { cursor: wait; opacity: .6; }
    </style>
  </body>
</html>
```

<span id="emitir-el-css-del-shell-en-archivos"></span>

<span class="section-anchor" id="section-11"></span>

### Emitir o CSS do shell em arquivos

Esta configuração faz o Astro gerar arquivos CSS externos, incluindo os estilos de `/contador/`. Assim, eles são carregados com a política de estilos de `wu serve`, que permite recursos da mesma origem. Mantenha essa opção ao personalizar a configuração do shell.

**`shell/astro.config.mjs`** · [Abrir arquivo completo](/examples/browser/shared-counter/shell/astro.config.mjs.txt)

```js
import { defineConfig } from 'astro/config';

export default defineConfig({
  server: { port: 4321 },
  build: { inlineStylesheets: 'never' },
});
```

<span id="6-comprobar-la-salida-de-producción"></span>

<span class="section-anchor" id="section-12"></span>

## 6. Verificar a saída de produção

Pare o servidor com **Ctrl+C** e execute na raiz:

```sh
wu build
wu serve 4100 --mode static
```

Abra [http://127.0.0.1:4100/contador/index.html](http://127.0.0.1:4100/contador/index.html) e repita as seis interações. Recarregue essa mesma rota: as duas apps devem carregar novamente e começar em zero. Você está servindo o `dist/` combinado do workspace.

`wu serve 4100 --mode static` resolve índices de diretório e retorna 404 reais para rotas inexistentes. Use `--mode spa` apenas quando rotas do cliente precisarem de fallback para o shell.

<span id="si-algo-no-coincide"></span>

<span class="section-anchor" id="section-13"></span>

### Se algo não corresponder

| O que você observa | O que revisar |
| --- | --- |
| `/contador/` mostra a página anterior ou não existe | Confira `shell/src/pages/contador.astro` e o build do shell no terminal. Se copiou os arquivos com o desenvolvimento parado, execute `npm --prefix shell run build` antes de iniciar. |
| Uma permissão é negada | Cada app deve ter seu `wu.json`; as concessões do host devem cobrir `store.read` e `store.write` de `demo.counter`. |
| Funciona no desenvolvimento, mas a permissão é negada em produção | Verifique se copiou ambos os `vite.config.js` e se `dist/mf-mi-plataforma/wu.json` e `dist/mf-inspector/wu.json` mantêm `wu.permissions`. |
| Apenas um contador muda | Substitua tanto `App` quanto `main` em cada app; os quatro arquivos devem corresponder a esta receita. |
| O valor inicial não aparece | A página deve inicializar o store antes da montagem; cada componente deve ler ao se conectar. |

O próximo passo é [Pedir uma soma a outra app](/pt/docs/guides/rpc-between-apps/): uma app oferece uma operação e outra pede seu resultado. Para aprofundar em caminhos, snapshots e notificações, consulte [Estado compartilhado](/pt/docs/framework/state/).