Un store portable, pas un renderer React ou Vue livré

Ces intégrations importent uniquement @newvexa/flowora. Aucun peer Angular ou ELK n’est nécessaire pour observer et éditer les documents. Elles illustrent une liste et des commandes, pas une réécriture cachée du canvas : le renderer fourni est Angular. Une interface de graphe native React/Vue, son accessibilité et ses gestes sont à construire dans votre hôte.

Le contrat est simple : editor.subscribe(listener: () => void) renvoie le désabonnement ; editor.getSnapshot() renvoie un EditorSnapshot immuable. Le callback n’a pas d’argument et l’état initial se lit directement. Le snapshot garde la même référence jusqu’à une notification : ne le recopiez pas dans le getter d’un store externe.

React : useSyncExternalStore

Le hook ci-dessous conserve les méthodes liées à l’instance avec des wrappers mémorisés. React gère le désabonnement de useSyncExternalStore. Le composant propriétaire crée l’éditeur dans un effet et le libère dans son cleanup : les setups répétés de Strict Mode reçoivent chacun une instance fraîche, sans réutiliser une instance déjà disposée.

TSX · composant client React complet
// FloworaPanel.tsx · composant client React avec son propre store.
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
import { Editor, createDemoDocument } from '@newvexa/flowora';
import type { EditorSnapshot } from '@newvexa/flowora';

export function useFloworaSnapshot(editor: Editor): EditorSnapshot {
  // Les wrappers conservent le this de l'instance ; ne passez pas la méthode nue.
  const subscribe = useCallback(
    (notify: () => void) => editor.subscribe(notify),
    [editor],
  );
  const getSnapshot = useCallback(() => editor.getSnapshot(), [editor]);
  return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}

function DocumentPanel({ editor }: { readonly editor: Editor }) {
  const snapshot = useFloworaSnapshot(editor);
  return (
    <section aria-label="Document Flowora">
      <h2>{snapshot.document.name}</h2>
      <p>{snapshot.document.nodes.length} nœuds · révision {snapshot.revision}</p>
      <button type="button" onClick={() => {
        editor.addFromCatalog('response', { x: 320, y: 520 });
      }}>Ajouter une réponse</button>
      <button type="button" disabled={!snapshot.canUndo}
        onClick={() => editor.undo()}>Annuler</button>
      <ul>{snapshot.document.nodes.map((node) => (
        <li key={node.id}>{node.name}</li>
      ))}</ul>
    </section>
  );
}

export default function FloworaPanel() {
  const [editor, setEditor] = useState<Editor | null>(null);
  useEffect(() => {
    // Création dans l'effet : chaque setup possède sa nouvelle instance.
    const instance = new Editor(createDemoDocument());
    setEditor(instance);
    return () => { instance.dispose(); };
  }, []);
  if (!editor) return <p>Préparation du document local…</p>;
  return <DocumentPanel editor={editor} />;
}

Ce composant initialise son store après le montage et affiche le même état de préparation côté serveur et au premier rendu client. Dans un framework hybride, placez-le dans une frontière client selon les règles de votre application. Le troisième argument du hook utilise le même getter ; si vous rendez directement un store fourni par un parent en SSR, créez-le par requête et hydratez avec un document initial identique. N’utilisez jamais un singleton serveur partagé entre utilisateurs.

Le hook emprunte l’éditeur : il ne l’appelle pas avec dispose() lorsque seul le panneau disparaît. C’est le propriétaire qui le libère. Un changement d’instance en prop provoque un nouveau branchement ; ne construisez pas l’éditeur dans le corps du composant à chaque rendu. Un autre abonnement créé dans useEffect doit retourner sa propre fonction de nettoyage.

Vue : shallowRef et onUnmounted

Un snapshot immuable ne nécessite pas de réactivité profonde. Ce composable crée un éditeur ordinaire, publie chaque snapshot dans une shallowRef et expose la ref en lecture seule. Appelez-le pendant setup, pour que le hook de cycle de vie soit enregistré sur le bon composant.

Vue · SFC avec composable propriétaire et cleanup
<!-- FloworaPanel.vue · composant client Vue -->
<script setup lang="ts">
import { onUnmounted, shallowReadonly, shallowRef } from 'vue';
import { Editor, createDemoDocument } from '@newvexa/flowora';
import type { EditorDocument, EditorSnapshot } from '@newvexa/flowora';

function useFloworaDocument(document: EditorDocument) {
  // Instance ordinaire, pas de proxy reactive(Editor).
  const editor = new Editor(document);
  const snapshot = shallowRef<EditorSnapshot>(editor.getSnapshot());
  const off = editor.subscribe(() => {
    snapshot.value = editor.getSnapshot();
  });

  onUnmounted(() => {
    off();
    editor.dispose(); // Ce composable possède cette instance.
  });
  return { editor, snapshot: shallowReadonly(snapshot) };
}

const { editor, snapshot } = useFloworaDocument(createDemoDocument());
function addResponse(): void {
  editor.addFromCatalog('response', { x: 320, y: 520 });
}
</script>

<template>
  <section aria-label="Document Flowora">
    <h2>{{ snapshot.document.name }}</h2>
    <p>{{ snapshot.document.nodes.length }} nœuds · révision {{ snapshot.revision }}</p>
    <button type="button" @click="addResponse">Ajouter une réponse</button>
    <button type="button" :disabled="!snapshot.canUndo" @click="editor.undo()">
      Annuler
    </button>
    <ul>
      <li v-for="node in snapshot.document.nodes" :key="node.id">{{ node.name }}</li>
    </ul>
  </section>
</template>

Ici le composable possède l’instance : onUnmounted désabonne puis appelle dispose(). Pour une variante qui reçoit l’éditeur d’un parent, ne nettoyez que son abonnement ; le parent décide de la durée de vie du store. Ne passez pas Editor dans reactive() et ne modifiez jamais snapshot.document directement. En SSR, gardez les instances isolées par requête et un document initial cohérent avec l’hydratation.

Observer un changement documentaire précis

onEvent peut distinguer une modification du document d’une sélection ou d’un viewport. L’événement contient seulement type et révision ; lisez le snapshot pour le contenu. Cette fonction peut être utilisée dans un effet React ou dans setup, à condition d’enregistrer son nettoyage.

TypeScript · abonnement documentaire emprunté
import type { Editor } from '@newvexa/flowora';

// Emprunte une instance et renvoie le nettoyage à son consommateur.
export function observeDocument(editor: Editor): () => void {
  return editor.onEvent((event) => {
    if (event.type === 'document.changed') {
      const document = editor.getSnapshot().document;
      console.log(document.id, document.nodes.length, event.revision);
    }
  });
}

Les transactions publient aussi des états intermédiaires. Un événement document.changed n’est pas une autorisation d’écrire immédiatement sur un serveur à chaque mouvement. Définissez dans l’hôte regroupement, confirmation, erreurs et persistance. Les gestes annulés et les échecs de stockage demandent le même soin quel que soit le framework.

Des commandes, pas des effets métier.

Ajouter une réponse ou éditer le script modifie le JSON. Ces exemples n’appellent aucun moteur, email, RAG ou service d’IA et ne sont pas exécutés par les pages de documentation.