diff --git a/src/hooks/useMarkdownToHTML.ts b/src/hooks/useMarkdownToHTML.ts
index b9e9d0d0..995bb31c 100644
--- a/src/hooks/useMarkdownToHTML.ts
+++ b/src/hooks/useMarkdownToHTML.ts
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef } from 'react'
+import React, { useState, useEffect, useRef, useMemo } from 'react'
import { RelayPool } from 'applesauce-relay'
import { extractNaddrUris, replaceNostrUrisInMarkdownWithProfileLabels } from '../utils/nostrUriResolver'
import { fetchArticleTitles } from '../services/articleTitleResolver'
@@ -23,6 +23,35 @@ export const useMarkdownToHTML = (
// Resolve profile labels progressively as profiles load
const { labels: profileLabels, loading: profileLoading } = useProfileLabels(markdown || '', relayPool)
+
+ // Create stable dependencies based on Map contents, not Map objects
+ // This prevents unnecessary reprocessing when Maps are recreated with same content
+ const profileLabelsKey = useMemo(() => {
+ return Array.from(profileLabels.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join('|')
+ }, [profileLabels])
+
+ const profileLoadingKey = useMemo(() => {
+ return Array.from(profileLoading.entries())
+ .filter(([, loading]) => loading)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([k]) => k)
+ .join('|')
+ }, [profileLoading])
+
+ const articleTitlesKey = useMemo(() => {
+ return Array.from(articleTitles.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join('|')
+ }, [articleTitles])
+
+ // Keep refs to latest Maps for processing without causing re-renders
+ const profileLabelsRef = useRef(profileLabels)
+ const profileLoadingRef = useRef(profileLoading)
+ const articleTitlesRef = useRef(articleTitles)
+
+ useEffect(() => {
+ profileLabelsRef.current = profileLabels
+ profileLoadingRef.current = profileLoading
+ articleTitlesRef.current = articleTitles
+ }, [profileLabels, profileLoading, articleTitles])
// Fetch article titles
useEffect(() => {
@@ -54,15 +83,27 @@ export const useMarkdownToHTML = (
return () => { isCancelled = true }
}, [markdown, relayPool])
- // Process markdown with progressive profile labels and article titles
+ // Track previous markdown and processed state to detect actual content changes
+ const previousMarkdownRef = useRef(markdown)
+ const processedMarkdownRef = useRef(processedMarkdown)
+
useEffect(() => {
- console.log(`[profile-loading-debug][markdown-to-html] Processing markdown, profileLabels=${profileLabels.size}, profileLoading=${profileLoading.size}, articleTitles=${articleTitles.size}`)
- console.log(`[profile-loading-debug][markdown-to-html] Clearing rendered HTML and processed markdown`)
- // Always clear previous render immediately to avoid showing stale content while processing
- setRenderedHtml('')
- setProcessedMarkdown('')
+ processedMarkdownRef.current = processedMarkdown
+ }, [processedMarkdown])
+
+ // Process markdown with progressive profile labels and article titles
+ // Use stable string keys instead of Map objects to prevent excessive reprocessing
+ useEffect(() => {
+ const labelsSize = profileLabelsRef.current.size
+ const loadingSize = profileLoadingRef.current.size
+ const titlesSize = articleTitlesRef.current.size
+ console.log(`[profile-loading-debug][markdown-to-html] Processing markdown, profileLabels=${labelsSize}, profileLoading=${loadingSize}, articleTitles=${titlesSize}`)
if (!markdown) {
+ setRenderedHtml('')
+ setProcessedMarkdown('')
+ previousMarkdownRef.current = markdown
+ processedMarkdownRef.current = ''
return
}
@@ -71,21 +112,27 @@ export const useMarkdownToHTML = (
const processMarkdown = () => {
try {
// Replace nostr URIs with profile labels (progressive) and article titles
+ // Use refs to get latest values without causing dependency changes
const processed = replaceNostrUrisInMarkdownWithProfileLabels(
markdown,
- profileLabels,
- articleTitles,
- profileLoading
+ profileLabelsRef.current,
+ articleTitlesRef.current,
+ profileLoadingRef.current
)
if (isCancelled) return
- console.log(`[profile-loading-debug][markdown-to-html] Processed markdown, loading states:`, Array.from(profileLoading.entries()).filter(([, l]) => l).map(([e]) => e.slice(0, 16) + '...'))
+ const loadingStates = Array.from(profileLoadingRef.current.entries())
+ .filter(([, l]) => l)
+ .map(([e]) => e.slice(0, 16) + '...')
+ console.log(`[profile-loading-debug][markdown-to-html] Processed markdown, loading states:`, loadingStates)
setProcessedMarkdown(processed)
+ processedMarkdownRef.current = processed
} catch (error) {
console.error(`[markdown-to-html] Error processing markdown:`, error)
if (!isCancelled) {
setProcessedMarkdown(markdown) // Fallback to original
+ processedMarkdownRef.current = markdown
}
}
@@ -101,12 +148,24 @@ export const useMarkdownToHTML = (
return () => cancelAnimationFrame(rafId)
}
+ // Only clear previous content if this is the first processing or markdown changed
+ // For profile updates, just reprocess without clearing to avoid flicker
+ const isMarkdownChange = previousMarkdownRef.current !== markdown
+ previousMarkdownRef.current = markdown
+
+ if (isMarkdownChange || !processedMarkdownRef.current) {
+ console.log(`[profile-loading-debug][markdown-to-html] Clearing rendered HTML and processed markdown (markdown changed: ${isMarkdownChange})`)
+ setRenderedHtml('')
+ setProcessedMarkdown('')
+ processedMarkdownRef.current = ''
+ }
+
processMarkdown()
return () => {
isCancelled = true
}
- }, [markdown, profileLabels, profileLoading, articleTitles])
+ }, [markdown, profileLabelsKey, profileLoadingKey, articleTitlesKey])
return { renderedHtml, previewRef, processedMarkdown }
}