Three rendering bugs in the chat markdown component, all interconnected, all caused by the same root issue: react-markdown v9 quietly changed how it passes information to custom components, and our code hadn't caught up. A reminder that dependency updates can silently break things without any error - until a user hits the right edge case.
The hydration error: HTML rules you forgot existed
React was throwing warnings: "In HTML, <pre> cannot be a descendant of <p>." This sounds like a pedantic browser complaint, but it actually caused visible layout flickers.
Here's the deal. HTML has rules about what elements can go inside other elements. A <p> (paragraph) tag can only contain inline elements - things like text, links, bold text, inline code. It cannot contain block elements like <pre> (preformatted code), <div>, or <table>. When the browser encounters this invalid nesting, it tries to fix it by rearranging the DOM, which causes React's hydration (the process of making server-rendered HTML interactive) to stumble. It recovers, but you get flickers.
This happened because during streaming, the AI's response is being parsed as markdown while it's still generating. The markdown parser sometimes sees incomplete content and makes temporary guesses about structure - placing a code element inside a paragraph, for instance. Our paragraph component rendered as <p>, the code component rendered a full code block with <pre> inside it, and suddenly you had <p><pre>...</pre></p>. Invalid HTML.
Fix: paragraphs now render as <div> instead of <p>. Same visual styling, but <div> can contain anything. It's a pragmatic trade-off - slightly less semantic HTML in exchange for zero nesting violations. There's an old internet principle called Postel's law that says "be conservative in what you send, be liberal in what you accept." Applied here: be liberal in what you accept from the markdown parser during streaming, because it's going to produce weird intermediate states and your renderer needs to handle them gracefully.
Inline code rendered as full code blocks with copy buttons
Every backtick-wrapped inline term like EventSource or text/event-stream was rendering as a full code block - bordered container, language label, copy button, the works. In a table comparing WebSockets and SSE, half the cells had copy buttons in them. It looked completely broken.
The cause was a silent API change. React-markdown v9 removed a prop called inline from code components. Our code checked this prop to decide whether something was inline code (like this) or a fenced code block (the big bordered boxes). Since the prop no longer existed, the check always returned false, and every single code element - inline or not - rendered as a full code block. No error message, no deprecation warning, just wrong behaviour. This is the worst kind of breaking change: the silent kind.
Fix: we replaced the broken detection with a two-step approach. First, the pre wrapper component (which the markdown parser only generates for fenced code blocks, never for inline code) is overridden to pass through transparently. Then the code component uses a simple heuristic: if the element has a language class (set by the syntax highlighter for tagged code blocks) or the content contains line breaks (multiline means it's a code block, not inline), render it as a full block with copy button. Otherwise, render it as a small inline pill. This correctly handles fenced blocks with a language tag, fenced blocks without one (detected by being multiline), and inline backtick code (single line, no language class).
Tables without any visual structure
Markdown tables rendered as plain unstyled text with no borders, no header distinction, no row separation. Just a blob of words vaguely aligned in columns.
Fix: added custom component overrides for the full table element tree - the table wrapper, header row, header cells, and data cells - each styled with Tailwind utilities. Tables now render inside a rounded bordered container with a subtle header background, muted header text, and divider lines between rows.
Also added a copy button that appears when you hover over the table (a pattern called progressive disclosure - hidden until you need it, visible when you do). The copy function walks the DOM (reads through all the table rows and cells), extracts the text content, and formats it as tab-separated values (TSV). This means it pastes cleanly into spreadsheets, Notion, or any text editor. Tabs between columns, newlines between rows.
What Claude did here
I sent a screenshot of the broken table with copy buttons in every cell. Claude identified all three issues from the screenshot and the React component stack trace: the nesting violation, the removed inline prop, and the unstyled table elements. It explained the react-markdown v9 change, proposed the heuristic detection approach, and implemented all three fixes. The table copy feature was a follow-up - "if I want to copy just the table, how do I do that?" - and Claude added the hover button with TSV extraction in one pass. This kind of workflow - screenshot, root cause analysis, fix - is where AI coding assistants are genuinely hard to beat. The combination of visual pattern recognition, codebase knowledge, and implementation speed in a single loop is remarkable.