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: hiddenpara 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
| Archivo | Cambio |
|---|---|
src/components/notebook/NoteEditor.tsx | Elimina condicionales React, añade requestMeasure() doble RAF |
src/styles/tools.css | Reglas CSS explícitas por modo para display de cada panel |
public/search-index.json | Regenerado automáticamente (sin cambio funcional) |
Véase también
- [[workspace—que-es-workspace]]
- [[workspace—noticias-alertas]]