diff --git a/.changeset/petite-cows-smoke.md b/.changeset/petite-cows-smoke.md
new file mode 100644
index 0000000..9a6f9c1
--- /dev/null
+++ b/.changeset/petite-cows-smoke.md
@@ -0,0 +1,5 @@
+---
+'@coldwired/react': minor
+---
+
+add fragment cache
diff --git a/packages/react/src/root.test.tsx b/packages/react/src/root.test.tsx
index b8ce3f0..7c1fade 100644
--- a/packages/react/src/root.test.tsx
+++ b/packages/react/src/root.test.tsx
@@ -76,6 +76,34 @@ describe('@coldwired/react', () => {
root.destroy();
});
+ it('render fragment with component and cache', async () => {
+ document.body.innerHTML = `<${DEFAULT_TAG_NAME}><${REACT_COMPONENT_TAG} ${NAME_ATTRIBUTE}="Counter">${REACT_COMPONENT_TAG}>${DEFAULT_TAG_NAME}>`;
+ const root = createRoot({
+ loader: (name) => Promise.resolve(manifest[name]),
+ cache: true,
+ });
+ await root.render(document.body).done;
+
+ expect(document.body.innerHTML).toEqual(
+ `<${DEFAULT_TAG_NAME} data-fragment-id="fragment-0">
${DEFAULT_TAG_NAME}>`,
+ );
+
+ root.destroy();
+
+ {
+ const root = createRoot({
+ loader: (name) => Promise.resolve(manifest[name]),
+ cache: true,
+ });
+ await root.render(document.body).done;
+
+ expect(document.body.innerHTML).toEqual(
+ `<${DEFAULT_TAG_NAME} data-fragment-id="fragment-0">${DEFAULT_TAG_NAME}>`,
+ );
+ root.destroy();
+ }
+ });
+
it('render with error boundary', async () => {
document.body.innerHTML = `<${DEFAULT_TAG_NAME}>
<${REACT_COMPONENT_TAG} ${NAME_ATTRIBUTE}="Counter">${REACT_COMPONENT_TAG}>
diff --git a/packages/react/src/root.ts b/packages/react/src/root.ts
index 9124934..6a08250 100644
--- a/packages/react/src/root.ts
+++ b/packages/react/src/root.ts
@@ -32,6 +32,7 @@ export interface RootOptions {
schema?: Partial;
layoutComponentName?: string;
errorBoundaryFallbackComponentName?: string;
+ cache?: boolean;
}
export interface Schema extends TreeBuilderSchema {
@@ -115,6 +116,11 @@ export function createRoot(
}
await preload(fragment, (names) => manifestLoader(names, loader, manifest), schema);
const tree = hydrate(fragment, manifest, schema);
+
+ if (options.cache) {
+ saveFragmentCache(element, fragmentOrHTML);
+ }
+
if (reset) {
element.innerHTML = '';
}
@@ -145,6 +151,9 @@ export function createRoot(
await mounting;
mounted.set(element, (fragmentOrHTML) => render(element, fragmentOrHTML, false));
registerDestroyer(element);
+ if (options.cache) {
+ restoreFragmentCache(element);
+ }
await render(element, element, true);
}
};
@@ -271,3 +280,48 @@ async function getErrorBoundaryFallbackComponent(
}
return;
}
+
+let fragmentCacheIdSequence = 0;
+const fragmentCacheIdAttributeName = 'data-fragment-id';
+const fragmentCache = new Map();
+
+export function resetFragmentCache() {
+ fragmentCache.clear();
+}
+
+function saveFragmentCache(element: Element, fragment: DocumentFragmentLike | string) {
+ let fragmentCacheId = element.getAttribute(fragmentCacheIdAttributeName);
+ if (!fragmentCacheId) {
+ fragmentCacheId = `fragment-${fragmentCacheIdSequence++}`;
+ element.setAttribute(fragmentCacheIdAttributeName, fragmentCacheId);
+ }
+ fragmentCache.set(fragmentCacheId, stringifyFragment(fragment));
+}
+
+function restoreFragmentCache(element: Element) {
+ const fragmentCacheId = element.getAttribute(fragmentCacheIdAttributeName);
+ if (fragmentCacheId) {
+ const html = fragmentCache.get(fragmentCacheId);
+ if (html) {
+ element.innerHTML = html;
+ }
+ }
+}
+
+function stringifyFragment(fragmentOrHTML: DocumentFragmentLike | string): string {
+ if (typeof fragmentOrHTML == 'string') {
+ return fragmentOrHTML;
+ }
+ if (isElement(fragmentOrHTML)) {
+ return fragmentOrHTML.innerHTML;
+ }
+ const html: string[] = [];
+ for (const node of fragmentOrHTML.childNodes) {
+ if (isElement(node)) {
+ html.push(node.outerHTML);
+ } else if (node.nodeType == Node.TEXT_NODE && node.textContent) {
+ html.push(node.textContent);
+ }
+ }
+ return html.join(' ');
+}