CreaRack-SL

Bug: toggle de Preview destruía el EditorView de CodeMirror en el Cuaderno

Resumen

Al cambiar entre modos de vista (Source / Preview / Split) en el Cuaderno de notas del Workspace, el componente NoteEditor desmontaba condicionalmente el div contenedor de CodeMirror mediante un condicional React ({viewMode !== 'preview' && <div ref={containerRef} />}). Esto destruía silenciosamente el EditorView de CodeMirror. Al volver al modo Source o Split, el div se remontaba pero el useEffect responsable de inicializar el editor estaba atado a [note.id] — como la nota no había cambiado, no se reejecuta, y el div quedaba vacío.

Impacto: el editor dejaba de funcionar para el resto de la sesión hasta que el usuario abría otra nota o recargaba la página.


Causa raíz

El problema tiene dos capas:

1. React unmount destruye el estado imperativo de CodeMirror

CodeMirror 6 (@codemirror/view) crea un EditorView que gestiona internamente su estado DOM. Cuando React desmonta el div al que está anclado el EditorView, éste no tiene mecanismo de pausa/reanudación: se destruye irreversiblemente. El patrón {condicion && <div ref={ref} />} es incompatible con editores stateful que mantienen referencias imperativas.

2. useEffect no reejectua al remontar si la dependencia no cambia

// ANTES del fix — incorrecto
useEffect(() => {
  // inicializa EditorView sobre containerRef.current
}, [note.id]); // solo se ejecuta si cambia la nota

Al volver a Source desde Preview, note.id no había cambiado, así que el efecto no corría. El div remontado quedaba vacío.


Fix aplicado (commit f0017ed)

Estrategia: mantener ambos paneles siempre montados, alternar visibilidad con CSS

// DESPUÉS del fix — correcto
<div className={`cuaderno-view cuaderno-view-${viewMode}`}>
  {/* Siempre montado — CodeMirror vive mientras dure la nota */}
  <div ref={containerRef} className="cuaderno-cm-container" />
  {/* Siempre montado — oculto con display:none en modo source */}
  <div
    className="cuaderno-preview"
    dangerouslySetInnerHTML={{ __html: previewHtml }}
    aria-label="Vista previa renderizada"
  />
</div>

El CSS en tools.css usa display: none / display: flex / display: block según la clase .cuaderno-view-{mode} del contenedor padre:

/* SOURCE: editor visible, preview oculto */
.cuaderno-view-source > .cuaderno-cm-container { display: flex; flex: 1; }
.cuaderno-view-source > .cuaderno-preview       { display: none; }

/* PREVIEW: preview visible, editor oculto */
.cuaderno-view-preview > .cuaderno-cm-container { display: none; }
.cuaderno-view-preview > .cuaderno-preview      { display: block; flex: 1; }

/* SPLIT: ambos al 50% */
.cuaderno-view-split > .cuaderno-cm-container,
.cuaderno-view-split > .cuaderno-preview        { display: flex/block; flex: 1 1 50%; }

Bonus: requestMeasure() tras reemerger del display:none

Cuando CodeMirror está oculto (display: none), su layout interno registra width = 0 y height = 0. Al volver a ser visible, los gutters y la selección de texto quedan desincronizados hasta que el usuario hace focus o dispara un resize. El fix fuerza una remedición usando doble RAF (Request Animation Frame) para garantizar que el CSS ya está aplicado antes de medir:

useEffect(() => {
  if (typeof window !== 'undefined')
    localStorage.setItem('workspace.notebook.viewMode', viewMode);

  if (editorView && viewMode !== 'preview') {
    requestAnimationFrame(() => {
      requestAnimationFrame(() => editorView.requestMeasure());
    });
  }
}, [viewMode, editorView]);

Patrón generalizable

Regla: nunca desmontéis condicionalmente el nodo DOM al que está anclado un editor stateful (CodeMirror, Monaco, Quill, ProseMirror, Slate…). Usad display: none / visibility: hidden para ocultarlo, o el API propio de pausa del editor si lo tiene.

Este patrón aplica a cualquier integración de editor de texto enriquecido en React donde el ciclo de vida del componente no coincide con el ciclo de vida del editor.


Archivos modificados

ArchivoCambio
src/components/notebook/NoteEditor.tsxElimina condicionales React, añade requestMeasure() doble RAF
src/styles/tools.cssReglas CSS explícitas por modo para display de cada panel
public/search-index.jsonRegenerado automáticamente (sin cambio funcional)

Véase también

  • [[workspace—que-es-workspace]]
  • [[workspace—noticias-alertas]]