Repository: Rezact/Rezact
Pull request #7 (2023-09-24 00:41:51)

List re render test


Patch:
diff --git a/package-lock.json b/package-lock.json
index c9ab312..a5d76dd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@rezact/rezact",
-  "version": "1.0.15-beta.6",
+  "version": "1.0.15-beta.8",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@rezact/rezact",
-      "version": "1.0.15-beta.6",
+      "version": "1.0.15-beta.8",
       "license": "MIT",
       "dependencies": {
         "@mdx-js/rollup": "^2.3.0",
diff --git a/package.json b/package.json
index c53534f..26ba8eb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@rezact/rezact",
-  "version": "1.0.15-beta.7",
+  "version": "1.0.15-beta.8",
   "type": "module",
   "description": "Intuitive Reactivity, Simplified State. Embrace a modern UI framework that encourages direct data mutations, offers module-level reactivity, and simplifies component design. With Rezact, you get the power of reactivity without the boilerplate. Dive into a seamless development experience tailored for today's web.",
   "main": "index.js",
diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx
index 365331c..d4dd181 100644
--- a/src/components/HomePage.tsx
+++ b/src/components/HomePage.tsx
@@ -54,7 +54,7 @@ function Button() {
               {$idx}. {$item} {$testToggle ? "On" : "Off"}
             </span>
             <button onClick={() => ($testToggle = !$testToggle)}>Toggle</button>
-            <button type="button" onClick={() => $arr.deleteValue($item)}>
+            <button type="button" onClick={() => $arr.splice($idx, 1)}>
               Delete
             </button>
             <br />
diff --git a/src/components/HomePageStatePersistsAcrossRouteChanges.tsx b/src/components/HomePageStatePersistsAcrossRouteChanges.tsx
index d7394ad..76b4c13 100644
--- a/src/components/HomePageStatePersistsAcrossRouteChanges.tsx
+++ b/src/components/HomePageStatePersistsAcrossRouteChanges.tsx
@@ -58,7 +58,7 @@ function Button() {
             <button onClick={() => ($item.$toggle = !$item.$toggle)}>
               Toggle
             </button>
-            <button type="button" onClick={() => $arr.deleteValue($item)}>
+            <button type="button" onClick={() => $arr.splice($idx, 1)}>
               Delete
             </button>
             <br />
diff --git a/src/components/appRouter.ts b/src/components/appRouter.ts
index 9b252fd..6ae7d67 100644
--- a/src/components/appRouter.ts
+++ b/src/components/appRouter.ts
@@ -59,6 +59,8 @@ router.addRoute(
 
 router.addRoute("/todos", () => import("src/examples/Todo/Todo"));
 
+router.addRoute("/list-redraw", () => import("src/examples/ListReRender/App"));
+
 router.addRoute(
   "/reactive-computations",
   () => import("src/examples/ReactiveComputations/ReactiveComp")
diff --git a/src/examples/Layout/nestedLayout.tsx b/src/examples/Layout/nestedLayout.tsx
index c4224ef..4ba8c91 100644
--- a/src/examples/Layout/nestedLayout.tsx
+++ b/src/examples/Layout/nestedLayout.tsx
@@ -62,6 +62,9 @@ export function MyLayout({ Component, pageProps }: any) {
         <li>
           <a href="/store-test">Store Test</a>
         </li>
+        <li>
+          <a href="/list-redraw">List Redraw</a>
+        </li>
         <li>
           <a href="/asdfasdfasdfasdf">A Route that does not exist</a>
         </li>
diff --git a/src/examples/ListReRender/App.tsx b/src/examples/ListReRender/App.tsx
new file mode 100644
index 0000000..47416c9
--- /dev/null
+++ b/src/examples/ListReRender/App.tsx
@@ -0,0 +1,94 @@
+import { MyLayout } from "../Layout/nestedLayout";
+import { $items, addItem } from "./store";
+
+export default function App() {
+  return (
+    <>
+      <h3>Cart</h3>
+      <ul>
+        {$items.map(($item, $idx) => (
+          <li>
+            <Editor $item={$item} $idx={$idx} />
+          </li>
+        ))}
+        <li>
+          <button onClick={addItem}>add one</button>
+        </li>
+      </ul>
+
+      <hr />
+
+      <h3>Details</h3>
+      <ul>
+        {$items.map(($item) => (
+          <li>
+            <Viewer $item={$item} />
+          </li>
+        ))}
+      </ul>
+
+      <h3>Total</h3>
+      <Result />
+    </>
+  );
+}
+
+function Display({ $value, $hue }) {
+  return (
+    <div style="display: flex; padding: 4px 0;">
+      <span style="width: 200px; word-wrap: break-word">{$value}</span>
+      <span style="display: flex; gap: 8px">
+        <div style={`width: 64px; background: hsl(${$hue}, 100%, 50%);`} />{" "}
+        &lt;- redraw
+      </span>
+    </div>
+  );
+}
+
+function Editor({ $item, $idx }) {
+  return (
+    <div style="display: flex; gap: 8px;">
+      <label>
+        price:
+        <input type="number" value={$item.$price} step="0.5" />
+      </label>
+      <label>
+        quantity:
+        <input type="number" value={$item.$qty} />
+      </label>
+      <button type="button" onClick={() => $items.splice($idx, 1)}>
+        remove
+      </button>
+    </div>
+  );
+}
+
+function Result() {
+  let $trigger = 0;
+  let $total = $items.reduce((acc, $item) => {
+    return acc + $item.$price * $item.$qty;
+  }, 0);
+
+  $: {
+    $total;
+    $trigger = Math.cos(Date.now() / 1000) * 0.5 + 0.5;
+  }
+  return <Display $value={$total} $hue={`${$trigger * 360}`} />;
+}
+
+function Viewer({ $item }) {
+  let $trigger = 0;
+  $: {
+    $item.$price;
+    $item.$qty;
+    $trigger = Math.cos(Date.now() / 1000) * 0.5 + 0.5;
+  }
+  return (
+    <Display
+      $value={`${$item.$price.getValue()}, x${$item.$qty.getValue()}`}
+      $hue={`${$trigger * 360}`}
+    />
+  );
+}
+
+export const Layout = MyLayout;
diff --git a/src/examples/ListReRender/store.ts b/src/examples/ListReRender/store.ts
new file mode 100644
index 0000000..42eae34
--- /dev/null
+++ b/src/examples/ListReRender/store.ts
@@ -0,0 +1,8 @@
+export let $items = [];
+
+export function addItem() {
+  $items.push({
+    $price: "2",
+    $qty: "1",
+  });
+}
diff --git a/src/lib/rezact/index.ts b/src/lib/rezact/index.ts
index daf54bd..ff80909 100644
--- a/src/lib/rezact/index.ts
+++ b/src/lib/rezact/index.ts
@@ -151,7 +151,8 @@ export function useInputs() {
       !Object.keys(attributes).includes("onChange") &&
       !Object.keys(attributes).includes("onInput")
     ) {
-      const evType = element.type === "text" ? "input" : "change";
+      const inpEvType = element.type === "text" || element.type === "number";
+      const evType = inpEvType ? "input" : "change";
       element.addEventListener(evType, () => {
         attributeValue.setValue(getInputVal(element));
       });
diff --git a/src/lib/rezact/mapState.ts b/src/lib/rezact/mapState.ts
index 67d30bc..c807dee 100644
--- a/src/lib/rezact/mapState.ts
+++ b/src/lib/rezact/mapState.ts
@@ -29,10 +29,85 @@ function _createComputed(func: (obj: any) => {}, deps: any[]) {
   return newState;
 }
 
+function findNestedStates(obj, results = [], skipKeys = []) {
+  if (obj instanceof Window) return [];
+  if (obj instanceof Document) return [];
+  if (obj instanceof HTMLElement) return [];
+  if (obj instanceof Node) return [];
+
+  // Check if the input is an object
+  if (typeof obj === "object" && obj !== null) {
+    // If it's an array, iterate over each element
+    if (Array.isArray(obj)) {
+      for (let item of obj) {
+        findNestedStates(item, results, skipKeys);
+      }
+    } else {
+      // If it's an object, iterate over each property
+      for (let key in obj) {
+        if (key.startsWith("$")) {
+          results.push(obj[key]);
+        }
+        if (skipKeys.includes(key)) continue;
+        if (obj[key] instanceof Window) continue;
+        if (obj[key] instanceof Document) continue;
+        if (obj[key] instanceof HTMLElement) continue;
+        if (obj[key] instanceof Node) continue;
+
+        findNestedStates(obj[key], results, skipKeys);
+      }
+    }
+  }
+  return results;
+}
+
+function findNestedMapStates(item) {
+  return findNestedStates(
+    item,
+    [],
+    [
+      "elmRef",
+      "idxState",
+      "deps",
+      "subs",
+      "func",
+      "associatedState",
+      "state",
+      "computed",
+    ]
+  );
+}
+
+function subscribeToNestedStates(item, mapStateObj) {
+  if (!(item.elmRef instanceof HTMLElement)) return;
+  const nestedStates = findNestedMapStates(item.value);
+  nestedStates.forEach((state) => {
+    item.nestedSubscribed = true;
+
+    state.subscribe(
+      () => {
+        if (
+          mapStateObj.deps &&
+          mapStateObj.deps.length > 0 &&
+          mapStateObj.deps[0] instanceof MapState
+        ) {
+          mapStateObj.deps[0].refresh();
+        } else {
+          mapStateObj.refresh();
+        }
+      },
+      { elm: item.elmRef }
+    );
+  });
+}
+
 export class MapState extends BaseState {
+  elmRefCache: any = new Map();
+  refreshTimer: any;
+
   constructor(st: any) {
     super(st);
-    ["push", "pop", "splice", "shift", "unshift"].forEach((item) => {
+    ["push", "pop", "splice", "shift", "unshift", "reduce"].forEach((item) => {
       this[item] = (...args) => {
         this.value[item](...args);
         this.updateList(this.func);
@@ -66,8 +141,25 @@ export class MapState extends BaseState {
         item.idxState.setValue(idx);
       }
 
-      if (item.elmRef?.isConnected === false) item.elmRef = undefined;
-      item.elmRef = item.elmRef || createComponent({ func, item });
+      let cachedElmRef = this.elmRefCache.get(item);
+      if (cachedElmRef?.isConnected === false) {
+        this.elmRefCache.delete(item);
+        cachedElmRef = undefined;
+      }
+
+      if (
+        item.elmRef?.isConnected === false ||
+        item.elmRef?.isConnected === undefined
+      )
+        item.elmRef = undefined;
+
+      item.elmRef =
+        cachedElmRef || item.elmRef || createComponent({ func, item });
+      if (!cachedElmRef) this.elmRefCache.set(item, item.elmRef);
+
+      if (!item.nestedSubscribed) {
+        subscribeToNestedStates(item, this);
+      }
 
       isArray(item.elmRef)
         ? item.elmRef.forEach((elm) => (elm.associatedState = item))
@@ -92,7 +184,8 @@ export class MapState extends BaseState {
   }
 
   refresh = () => {
-    setTimeout(() => {
+    if (this.refreshTimer) clearTimeout(this.refreshTimer);
+    this.refreshTimer = setTimeout(() => {
       this.updateList(this.func);
       this.alertSubs(this.value);
     }, 10);
@@ -172,7 +265,7 @@ const removeStaleChildren = (parentNode, endNode, parent, child) => {
         prevElm.replaceWith(elmRef);
         remPlaceHolder.replaceWith(prevElm);
       } else {
-        elmRef.remove();
+        if (elmRef instanceof HTMLElement) elmRef.remove();
       }
     } else {
       nextNode = nextNode.nextSibling;
@@ -239,6 +332,9 @@ function appendChildNode(
   removeElm: boolean = false
 ) {
   if (removeElm) return childNode.remove();
+  if (parentNode instanceof Comment) insertAfter = true;
+  if (parentNode.state) return;
+  // console.log({ parentNode, childNode, insertAfter });
   insertAfter
     ? insertNodeAfter(parentNode, childNode)
     : parentNode.appendChild(childNode);
@@ -263,5 +359,6 @@ childArrayHandler.handler = (parent, child, insertAfter, removeElm) => {
 export const createMapped = (func, deps) => {
   const computed: any = createComputed(func, deps);
   computed.deps = deps;
+  computed.mapStateObj = false;
   return computed;
 };
diff --git a/src/lib/rezact/vite-plugin.ts b/src/lib/rezact/vite-plugin.ts
index 98eacf3..fd1f9d8 100644
--- a/src/lib/rezact/vite-plugin.ts
+++ b/src/lib/rezact/vite-plugin.ts
@@ -13,6 +13,7 @@ let importsUsed: any = {};
 let signalsUsed: any = {};
 let mapStateUsed: any = {};
 let functionsToRun: any = [];
+let mapDeclarationTracking = {};
 
 function wrapInUseSignal(node) {
   signalsUsed.BaseState = true;
@@ -25,7 +26,23 @@ function wrapInUseSignal(node) {
   }
 }
 
-function wrapInUseMapState(node) {
+function nodeWillWrapInCreateMapped(node) {
+  return (
+    node.callee.type === "MemberExpression" &&
+    ((node.callee.object.type === "Identifier" &&
+      node.callee.object.name[0] === "$") ||
+      (node.callee.object.type === "MemberExpression" &&
+        node.callee.object.property.name &&
+        node.callee.object.property.name[0] === "$")) &&
+    node.callee.property.name === "map"
+  );
+}
+
+function wrapInUseMapState(node, fullNode, objectNode = null) {
+  const mapName =
+    fullNode.id?.name || objectNode?.id?.name + "." + fullNode.key?.value;
+
+  if (mapName) mapDeclarationTracking[mapName] = objectNode || fullNode;
   mapStateUsed.MapState = true;
   magicString.appendLeft(node.start, `new MapState(`);
   magicString.appendRight(node.end, `)`);
@@ -60,9 +77,13 @@ function findDependencies(startingNode, excludeDeps = {}) {
       ] = `${node.object.name}.getValue().${node.property.name}`;
     },
 
-    Identifier(node: any) {
+    Identifier(node: any, _state, ancestors) {
       // if (hasAncestor(ancestors, "MemberExpression")) return;
       if (node.name && node.name[0] !== "$") return;
+      if (ancestors.at(-2)?.type === "TemplateLiteral" && !ancestors.at(-3)) {
+        appendGetValue(node);
+      }
+
       identifiers[node.name] = true;
     },
   });
@@ -120,16 +141,47 @@ function wrapInCreateComputedAttribute(
 
 function wrapInCreateMapped(node, explicitDeps = null, excludeDeps = {}) {
   mapStateUsed.createMapped = true;
+
   const _deps = explicitDeps || findDependencies(node, excludeDeps);
   if (_deps.length === 0) return;
   const args = _deps.map((dep) => (dep.includes(".") ? "arr" : dep));
   const deps = _deps.map((dep) => dep.arg || dep);
   signalsUsed.createComputed = true;
+  let mapStateObj = "";
+  if (node.callee.upgradedToRezactMap) {
+    const uuid = Math.random().toString(36).slice(-10).replace(".", "x");
+
+    const dependentArg =
+      node.callee?.object?.name || node.callee?.object?.property?.name;
+    const dependentMap =
+      node.callee?.object?.name ||
+      node.callee?.object?.object?.name +
+        "." +
+        node.callee?.object?.property?.name;
+
+    magicString.overwrite(
+      node.callee.object.start,
+      node.callee.object.end,
+      "$m" + uuid
+    );
+    const declaredMap = mapDeclarationTracking[dependentMap];
+    const decToRun = `let $m${uuid} = createMapped(([${dependentArg}]) => ${dependentMap}.Map((item) => item), [${dependentMap}] );`;
+    if (declaredMap) {
+      magicString.appendLeft(declaredMap.end + 1, decToRun);
+    } else {
+      functionsToRun.push(decToRun);
+    }
+    // magicString.appendRight(node.arguments[0].end, ", $m" + uuid);
+    args[0] = `$m${uuid}`;
+    deps[0] = `$m${uuid}`;
+    // mapStateObj = `, $m${uuid}`;
+  }
+
   magicString.appendLeft(node.start, `createMapped(([${args.join(",")}]) => `);
-  magicString.appendRight(node.end, `, [${deps.join(",")}])`);
+  magicString.appendRight(node.end, `, [${deps.join(",")}] ${mapStateObj})`);
 }
 
-function tackOnDotVee(node) {
+function appendGetValue(node) {
   if (node.wrappedInBaseState) return;
   node.vTackedOn = true;
   magicString.appendRight(node.end, `.getValue()`);
@@ -350,6 +402,14 @@ function wrapInSetValue(node, nestedMember = false) {
   );
 }
 
+function isReactiveAttribute(node) {
+  if (node.type !== "Property") return false;
+  if (!node.key) return false;
+  if (!node.key.name) return false;
+  if (node.key.name[0] !== "$") return false;
+  return true;
+}
+
 const arrProps = ["push", "pop", "splice", "shift", "unshift"];
 
 function compileRezact(ast) {
@@ -362,14 +422,27 @@ function compileRezact(ast) {
         if (node.init.type === "UnaryExpression") wrapInUseSignal(node.init);
         if (node.init.type === "BinaryExpression")
           wrapInCreateComputed(node.init);
-        if (node.init.type === "ArrayExpression") wrapInUseMapState(node.init);
-        if (node.init.type === "CallExpression")
-          wrapInCreateComputed(node.init);
+        if (node.init.type === "ArrayExpression")
+          wrapInUseMapState(node.init, node);
+        if (
+          node.init.type === "CallExpression" &&
+          !nodeWillWrapInCreateMapped(node.init)
+        ) {
+          const hasMapDecl =
+            mapDeclarationTracking[node.init?.callee?.object?.name];
+          if (hasMapDecl) {
+            const mapName = node.id?.name;
+            if (mapName) mapDeclarationTracking[mapName] = node;
+            wrapInCreateMapped(node.init);
+          } else {
+            wrapInCreateComputed(node.init);
+          }
+        }
       }
     },
 
     TemplateLiteral(node: any, _state, ancestors) {
-      if (isAttributeArg(ancestors)) {
+      if (isAttributeArg(ancestors) && !isReactiveAttribute(ancestors.at(-2))) {
         wrapInCreateComputedAttribute(node);
       } else {
         wrapInCreateComputed(node);
@@ -386,7 +459,7 @@ function compileRezact(ast) {
           prop.value.name[0] === "$" &&
           !isAttributeArg(ancestors)
         ) {
-          tackOnDotVee(prop.value);
+          appendGetValue(prop.value);
         }
 
         if (
@@ -397,7 +470,7 @@ function compileRezact(ast) {
         ) {
           prop.value.elements.forEach((elem) => {
             if (elem.type === "Identifier" && elem.name[0] === "$") {
-              tackOnDotVee(elem);
+              appendGetValue(elem);
             }
           });
         }
@@ -413,13 +486,13 @@ function compileRezact(ast) {
         ancestors.at(-2).type === "LogicalExpression" &&
         ancestors.at(-2).operator === "&&"
       ) {
-        tackOnDotVee(node.argument);
+        appendGetValue(node.argument);
       }
     },
 
     LogicalExpression(node: any, _state, ancestors: any) {
       if (node.left.type === "Identifier" && node.left.name[0] === "$") {
-        tackOnDotVee(node.left);
+        appendGetValue(node.left);
       }
 
       const lastAncestor = ancestors.at(-1);
@@ -476,7 +549,7 @@ function compileRezact(ast) {
         node.computed === true &&
         node.optional === false
       ) {
-        tackOnDotVee(node.object);
+        appendGetValue(node.object);
       }
 
       if (!node.property || !node.property.type || !node.property.name) return;
@@ -498,13 +571,13 @@ function compileRezact(ast) {
         node.property.type === "Identifier" &&
         node.property.name[0] === "$"
       ) {
-        tackOnDotVee(node.object);
+        appendGetValue(node.object);
 
         if (ancestors.at(-3).type === "TemplateLiteral")
-          tackOnDotVee(node.property);
+          appendGetValue(node.property);
 
         if (!isChildArg(ancestors) && !isAttributeArg(ancestors))
-          tackOnDotVee(node.property);
+          appendGetValue(node.property);
 
         // code the I wrote to use the ancestor function commented out above
         // but did not end up needing, keeping as an example of how to use
@@ -524,7 +597,7 @@ function compileRezact(ast) {
         //     "asdf",
         //     src.slice(node.property.start, node.property.end)
         //   );
-        //   tackOnDotVee(node.property);
+        //   appendGetValue(node.property);
         // }
       } else if (
         node.property.type === "Identifier" &&
@@ -548,18 +621,18 @@ function compileRezact(ast) {
           return wrapInSetValue(ancestors[anceLen], node.property);
         }
 
-        tackOnDotVee(node.property);
+        appendGetValue(node.property);
       } else if (
         node.object.name &&
         node.object.name[0] === "$" &&
         !propertyExcludeList.includes(node.property.name)
       ) {
-        tackOnDotVee(node.object);
-        if (node.computed) tackOnDotVee(node);
+        appendGetValue(node.object);
+        if (node.computed) appendGetValue(node);
       }
     },
 
-    CallExpression(node: any) {
+    CallExpression(node: any, _state) {
       if (node.arguments) {
         node.arguments.forEach((arg) => {
           if (
@@ -568,31 +641,25 @@ function compileRezact(ast) {
             node.callee?.object?.name === "console" &&
             node.callee?.property?.name === "log"
           ) {
-            tackOnDotVee(arg);
+            appendGetValue(arg);
           } else if (
             node.callee.type === "MemberExpression" &&
             arrProps.includes(node.callee.property.name) &&
             arg.type === "Identifier" &&
             arg.name[0] === "$"
           ) {
-            tackOnDotVee(arg);
+            appendGetValue(arg);
           }
         });
       }
-      if (
-        node.callee.type === "MemberExpression" &&
-        ((node.callee.object.type === "Identifier" &&
-          node.callee.object.name[0] === "$") ||
-          (node.callee.object.type === "MemberExpression" &&
-            node.callee.object.property.name &&
-            node.callee.object.property.name[0] === "$")) &&
-        node.callee.property.name === "map"
-      ) {
+      if (nodeWillWrapInCreateMapped(node)) {
         magicString.overwrite(
           node.callee.property.start,
           node.callee.property.end,
           "Map"
         );
+        node.callee.upgradedToRezactMap = true;
+
         wrapInCreateMapped(node, [
           node.callee.object.name ||
             src.slice(node.callee.object.start, node.callee.object.end),
@@ -600,7 +667,10 @@ function compileRezact(ast) {
       }
     },
 
-    Property(node: any, _state) {
+    Property(node: any, _state, ancestors: any) {
+      // if (isAttributeArg(ancestors)) {
+      //   console.log(src.slice(node.key.start - 5, node.key.end + 5));
+      // }
       if (node.key.name === "className") {
         magicString.overwrite(node.key.start, node.key.end, "class");
       }
@@ -610,7 +680,7 @@ function compileRezact(ast) {
         node.key.name[0] === "$" &&
         node.value.type === "ArrayExpression"
       ) {
-        return wrapInUseMapState(node.value);
+        return wrapInUseMapState(node.value, node, ancestors.at(-3));
       }
 
       if (
@@ -618,10 +688,14 @@ function compileRezact(ast) {
         node.key.value[0] === "$" &&
         node.value.type === "ArrayExpression"
       ) {
-        return wrapInUseMapState(node.value);
+        return wrapInUseMapState(node.value, node, ancestors.at(-3));
       }
 
-      if (node.key.name && node.key.name[0] === "$") {
+      if (
+        node.key.name &&
+        node.key.name[0] === "$" &&
+        !isAttributeArg(ancestors)
+      ) {
         wrapInUseSignal(node.value);
       }
       if (node.key.value && node.key.value[0] === "$") {
@@ -651,7 +725,7 @@ function compileRezact(ast) {
       }
       if (!node?.test?.name) return;
       if (node.test.name && node.test.name[0] === "$") {
-        tackOnDotVee(node.test);
+        appendGetValue(node.test);
       }
     },
 
@@ -662,7 +736,7 @@ function compileRezact(ast) {
     BinaryExpression(node: any, _state, ancestors: any) {
       const name = node.left.name;
       if (name && name[0] === "$") {
-        tackOnDotVee(node.left);
+        appendGetValue(node.left);
       }
 
       if (
@@ -731,6 +805,7 @@ function rezact(): PluginOption {
       lastImport = { end: 0 };
       importsUsed = {};
       functionsToRun = [];
+      mapDeclarationTracking = {};
       // console.log(id);
       magicString = new MagicString(src);
       const ast = acorn.parse(src, {


Test patch:
diff --git a/src/components/mdx.test.js b/src/components/mdx.test.js
index e0273f9..e9da4e8 100644
--- a/src/components/mdx.test.js
+++ b/src/components/mdx.test.js
@@ -11,6 +11,8 @@ describe("MDX suite", () => {
   it("renders MDX", async () => {
     render(document.body, TestMdx);
 
+    // await waitFor(() => expect(document.body.innerHTML).toMatchSnapshot());
+    await delay(100);
     expect(document.body.innerHTML).toMatchSnapshot();
 
     const allHeaders = await screen.findAllByRole("heading", {
@@ -45,3 +47,7 @@ describe("MDX suite", () => {
     );
   });
 });
+
+function delay(n) {
+  return new Promise((resolve) => setTimeout(resolve, n));
+}
diff --git a/src/examples/ListReRender/stores.test.ts b/src/examples/ListReRender/stores.test.ts
new file mode 100644
index 0000000..f16afc5
--- /dev/null
+++ b/src/examples/ListReRender/stores.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, it } from "vitest";
+// import { waitFor } from "@testing-library/dom";
+import { render } from "rezact";
+import Page from "./App";
+
+describe("List Re-Render Tests", () => {
+  it("items do not cause full list to re render", async () => {
+    render(document.body, Page);
+    expect(1).toBe(1);
+  });
+});