Files
kandev/apps/web/components/shared/memoized-markdown.tsx
T
2026-09-05 13:26:25 +00:00

65 lines
2.0 KiB
TypeScript

"use client";
import { memo, useContext, useMemo } from "react";
import ReactMarkdown, { type Components } from "react-markdown";
import {
MarkdownFileLinkContext,
MarkdownTaskContext,
markdownComponents,
remarkPlugins,
type MarkdownFileLinkContextValue,
} from "@/components/shared/markdown-components";
import { normalizeCached } from "@/lib/markdown/normalize-cache";
/**
* Markdown renderer behind a `memo` boundary keyed on the `content` string.
*
* `content` is a primitive, so React compares it by value. Keep optional
* file-link props stable at the caller when possible; identical props bail out
* of the memo and re-use the previously parsed element tree. The normalized
* string itself is also cached (LRU) so two messages with the same content
* share a single normalize pass.
*/
type MemoizedMarkdownProps = MarkdownFileLinkContextValue & {
content: string;
components?: Components;
taskId?: string | null;
};
export const MemoizedMarkdown = memo(function MemoizedMarkdown({
content,
worktreePath,
onOpenFile,
fileRootAliases,
components,
taskId = null,
}: MemoizedMarkdownProps) {
const inheritedContext = useContext(MarkdownFileLinkContext);
const fileLinkContext = useMemo(
() => ({
worktreePath: worktreePath ?? inheritedContext.worktreePath,
onOpenFile: onOpenFile ?? inheritedContext.onOpenFile,
fileRootAliases: fileRootAliases ?? inheritedContext.fileRootAliases,
}),
[
fileRootAliases,
inheritedContext.fileRootAliases,
inheritedContext.onOpenFile,
inheritedContext.worktreePath,
onOpenFile,
worktreePath,
],
);
const resolvedComponents = components ?? markdownComponents;
return (
<MarkdownTaskContext.Provider value={taskId}>
<MarkdownFileLinkContext.Provider value={fileLinkContext}>
<ReactMarkdown remarkPlugins={remarkPlugins} components={resolvedComponents}>
{normalizeCached(content)}
</ReactMarkdown>
</MarkdownFileLinkContext.Provider>
</MarkdownTaskContext.Provider>
);
});