diff --git a/packages/elements/__tests__/jsx-preview.test.tsx b/packages/elements/__tests__/jsx-preview.test.tsx
index 2b87d51d..8c1dd219 100644
--- a/packages/elements/__tests__/jsx-preview.test.tsx
+++ b/packages/elements/__tests__/jsx-preview.test.tsx
@@ -1,6 +1,8 @@
import { render, screen } from "@testing-library/react";
+import { StrictMode } from "react";
import type { JSXPreviewProps } from "../src/jsx-preview";
+
import {
JSXPreview,
JSXPreviewContent,
@@ -277,6 +279,70 @@ describe("jSXPreview streaming mode", () => {
);
expect(screen.getByText("Text")).toBeInTheDocument();
});
+
+ it("keeps the last good preview when a streamed chunk fails to parse", () => {
+ // Parser reports errors synchronously during render; the resulting React
+ // "update while rendering" dev warning is expected here
+ const consoleError = vi.spyOn(console, "error").mockImplementation(vi.fn());
+ const onError = vi.fn();
+
+ const { rerender } = render(
+
+
+
+ );
+ expect(screen.getByText("Good")).toBeInTheDocument();
+
+ rerender(
+
+
+
+ );
+ expect(screen.getByText("Good")).toBeInTheDocument();
+ expect(onError).not.toHaveBeenCalled();
+
+ rerender(
+
+
+
+ );
+ expect(screen.getByText("Recovered")).toBeInTheDocument();
+
+ consoleError.mockRestore();
+ });
+
+ it("keeps the last good preview across a bad chunk in Strict Mode", () => {
+ const consoleError = vi.spyOn(console, "error").mockImplementation(vi.fn());
+
+ const { rerender } = render(
+
+
+
+
+
+ );
+ expect(screen.getByText("Good")).toBeInTheDocument();
+
+ rerender(
+
+
+
+
+
+ );
+ expect(screen.getByText("Good")).toBeInTheDocument();
+
+ rerender(
+
+
+
+
+
+ );
+ expect(screen.getByText("Recovered")).toBeInTheDocument();
+
+ consoleError.mockRestore();
+ });
});
describe("jSXPreview integration", () => {
diff --git a/packages/elements/src/jsx-preview.tsx b/packages/elements/src/jsx-preview.tsx
index 22709929..6446b2cc 100644
--- a/packages/elements/src/jsx-preview.tsx
+++ b/packages/elements/src/jsx-preview.tsx
@@ -1,8 +1,10 @@
"use client";
+import type { ComponentProps, ReactNode } from "react";
+import type { TProps as JsxParserProps } from "react-jsx-parser";
+
import { cn } from "@repo/shadcn-ui/lib/utils";
import { AlertCircle } from "lucide-react";
-import type { ComponentProps, ReactNode } from "react";
import {
createContext,
memo,
@@ -13,7 +15,6 @@ import {
useRef,
useState,
} from "react";
-import type { TProps as JsxParserProps } from "react-jsx-parser";
import JsxParser from "react-jsx-parser";
interface JSXPreviewContextValue {
@@ -209,12 +210,17 @@ export const JSXPreviewContent = memo(
const errorReportedRef = useRef(null);
const lastGoodJsxRef = useRef("");
const [hadError, setHadError] = useState(false);
-
- // Reset error tracking when jsx changes
- useEffect(() => {
+ const [prevProcessedJsx, setPrevProcessedJsx] = useState(processedJsx);
+
+ // Reset error tracking before the parser renders the new string
+ // (react-jsx-parser reports parse errors synchronously during render,
+ // so a post-render effect would reset them too late and the tracking
+ // effect below would record the broken string as "last good")
+ if (processedJsx !== prevProcessedJsx) {
+ setPrevProcessedJsx(processedJsx);
errorReportedRef.current = null;
setHadError(false);
- }, [processedJsx]);
+ }
const handleError = useCallback(
(err: Error) => {