diff --git a/.changeset/fresh-code-blocks-update.md b/.changeset/fresh-code-blocks-update.md
new file mode 100644
index 00000000..8842c8c6
--- /dev/null
+++ b/.changeset/fresh-code-blocks-update.md
@@ -0,0 +1,5 @@
+---
+"streamdown": patch
+---
+
+Fix streamed code blocks remaining stale when their content changes without moving in the document.
diff --git a/packages/streamdown/__tests__/code-block-memo.test.tsx b/packages/streamdown/__tests__/code-block-memo.test.tsx
index 4c8675df..3bb020e3 100644
--- a/packages/streamdown/__tests__/code-block-memo.test.tsx
+++ b/packages/streamdown/__tests__/code-block-memo.test.tsx
@@ -18,6 +18,30 @@ vi.mock("../lib/code-block", () => ({
}));
describe("MemoCode in streaming mode", () => {
+ it("should re-render code blocks when their content changes in place", async () => {
+ codeBlockRenderCount = 0;
+
+ const initial = "```js\nfirst\n```";
+ const updated = "```js\nsecond\n```";
+
+ const { container, rerender } = render({initial});
+
+ await waitFor(() => {
+ expect(container.textContent).toContain("first");
+ });
+
+ const initialRenderCount = codeBlockRenderCount;
+
+ rerender({updated});
+
+ await waitFor(() => {
+ expect(container.textContent).toContain("second");
+ });
+
+ expect(container.textContent).not.toContain("first");
+ expect(codeBlockRenderCount).toBeGreaterThan(initialRenderCount);
+ });
+
it("should not re-render unchanged code blocks on streaming updates", async () => {
codeBlockRenderCount = 0;
diff --git a/packages/streamdown/lib/components.tsx b/packages/streamdown/lib/components.tsx
index cb6eb67d..396c919d 100644
--- a/packages/streamdown/lib/components.tsx
+++ b/packages/streamdown/lib/components.tsx
@@ -89,6 +89,38 @@ function sameClassAndNode(
);
}
+function getCodeContent(children: React.ReactNode): string | undefined {
+ if (typeof children === "string") {
+ return children;
+ }
+
+ if (
+ isValidElement(children) &&
+ children.props &&
+ typeof children.props === "object" &&
+ "children" in children.props &&
+ typeof children.props.children === "string"
+ ) {
+ return children.props.children;
+ }
+
+ return undefined;
+}
+
+function sameCodeContent(
+ prev: React.ReactNode,
+ next: React.ReactNode
+): boolean {
+ const prevContent = getCodeContent(prev);
+ const nextContent = getCodeContent(next);
+
+ if (prevContent === undefined && nextContent === undefined) {
+ return prev === next;
+ }
+
+ return prevContent === nextContent;
+}
+
const shouldShowControls = (
config: ControlsConfig,
type: "table" | "code" | "mermaid" | "image"
@@ -852,19 +884,7 @@ const CodeComponent = ({
: false;
const showLineNumbers = !metaNoLineNumbers && contextLineNumbers !== false;
- // Extract code content from children safely
- let code = "";
- if (
- isValidElement(children) &&
- children.props &&
- typeof children.props === "object" &&
- "children" in children.props &&
- typeof children.props.children === "string"
- ) {
- code = children.props.children;
- } else if (typeof children === "string") {
- code = children;
- }
+ const code = getCodeContent(children) ?? "";
if (customRenderer) {
const CustomComponent = customRenderer.component;
@@ -981,7 +1001,10 @@ const MemoCode = memo<
DetailedHTMLProps, HTMLElement> & ExtraProps
>(
CodeComponent,
- (p, n) => p.className === n.className && sameNodePosition(p.node, n.node)
+ (p, n) =>
+ p.className === n.className &&
+ sameNodePosition(p.node, n.node) &&
+ sameCodeContent(p.children, n.children)
);
MemoCode.displayName = "MarkdownCode";