@ryupold/vode 1.8.7 → 1.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryupold/vode",
3
- "version": "1.8.7",
3
+ "version": "1.8.10",
4
4
  "description": "a minimalist web framework",
5
5
  "author": "Michael Scherbakow (ryupold)",
6
6
  "license": "MIT",
@@ -25,11 +25,10 @@
25
25
  },
26
26
  "homepage": "https://github.com/ryupold/vode#readme",
27
27
  "type": "module",
28
- "main": "./dist/vode.mjs",
29
- "types": "./dist/vode.d.ts",
30
28
  "exports": {
31
29
  ".": {
32
30
  "types": "./dist/vode.d.ts",
31
+ "tests": "./dist/vode.tests.mjs",
33
32
  "import": "./dist/vode.min.mjs",
34
33
  "require": "./dist/vode.cjs.min.js",
35
34
  "default": "./dist/vode.min.js"
@@ -43,11 +42,12 @@
43
42
  "build-classic-min": "esbuild index.ts --outfile=dist/vode.min.js --bundle --format=iife --global-name=V --minify",
44
43
  "babel": "npx babel dist/vode.mjs --out-file dist/vode.cjs.min.js",
45
44
  "babel-classic": "npx babel dist/vode.js --out-file dist/vode.es5.min.js",
46
- "release": "npm run build && npm run build-min && npm run build-classic && npm run build-classic-min && npm run babel && npm run babel-classic && npm run types",
45
+ "release": "npm run build && npm run build-min && npm run build-classic && npm run build-classic-min && npm run babel && npm run babel-classic && npm run types && npm run build-tests",
47
46
  "publish": "npm publish --access public",
48
47
  "clean": "tsc -b --clean && rm dist/*",
49
48
  "watch": "tsc -b -w",
50
- "test": "tsc -p tsconfig.test.json && esbuild test/index.ts --bundle --outfile=test/bundle.js --platform=node && node test/bundle.js"
49
+ "test": "tsc -p tsconfig.test.json && esbuild test/run-tests.ts --bundle --outfile=test/run-tests.js --platform=node && node test/run-tests.js",
50
+ "build-tests": "esbuild test/index.ts --bundle --format=esm --outfile=dist/vode.tests.mjs"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@babel/cli": "7.28.6",
@@ -41,13 +41,13 @@ export interface SubContext<SubState> {
41
41
  }
42
42
 
43
43
  export type ProxyStateContext<S extends PatchableState, SubState> = StateContext<S, SubState> & {
44
- [K in keyof SubState]-?: SubState[K] extends object
44
+ [K in keyof SubState]-?: SubState[K] extends object | null
45
45
  ? ProxyStateContext<S, SubState[K]>
46
46
  : StateContext<S, SubState[K]>
47
47
  };
48
48
 
49
49
  export type ProxySubContext<SubState> = SubContext<SubState> & {
50
- [K in keyof SubState]-?: SubState[K] extends object
50
+ [K in keyof SubState]-?: SubState[K] extends object | null
51
51
  ? ProxySubContext<SubState[K]>
52
52
  : SubContext<SubState[K]>
53
53
  };
@@ -107,10 +107,12 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
107
107
  }
108
108
  raw[keys[i]] = value;
109
109
  } else if (keys.length === 1) {
110
- if (typeof (<any>target)[keys[0]] === "object" && typeof value === "object")
110
+ if (typeof (<any>target)[keys[0]] === "object" && typeof value === "object" && value !== null) {
111
111
  Object.assign((<any>target)[keys[0]], value);
112
- else
112
+ }
113
+ else {
113
114
  (<any>target)[keys[0]] = value;
115
+ }
114
116
  } else {
115
117
  Object.assign(target, value as DeepPartial<S>);
116
118
  }
package/src/vode.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type Vode<S = PatchableState> = FullVode<S> | JustTagVode | NoPropsVode<S>;
2
- export type FullVode<S = PatchableState> = [tag: Tag, props: Props<S>, ...children: ChildVode<S>[]];
2
+ export type FullVode<S = PatchableState> = [tag: Tag, props: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]];
3
3
  export type NoPropsVode<S = PatchableState> = [tag: Tag, ...children: ChildVode<S>[]] | (TextVode[]);
4
4
  export type JustTagVode = [tag: Tag];
5
5
  export type ChildVode<S = PatchableState> = Vode<S> | TextVode | NoVode | Component<S>;
@@ -7,7 +7,7 @@ export type TextVode = string & {};
7
7
  export type NoVode = undefined | null | number | boolean | bigint | void;
8
8
  export type AttachedVode<S> = Vode<S> & { node: ChildNode } | Text & { node?: never };
9
9
  export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap) | (string & {});
10
- export type Component<S> = (s: S) => ChildVode<S>;
10
+ export type Component<S = PatchableState> = (s: S) => ChildVode<S>;
11
11
 
12
12
  export type Patch<S> =
13
13
  | IgnoredPatch // ignored
@@ -37,9 +37,9 @@ export interface Props<S = PatchableState> extends Partial<
37
37
  class?: ClassProp,
38
38
  style?: StyleProp,
39
39
  /** called after the element was attached */
40
- onMount?: MountFunction<S>,
40
+ onMount?: MountFunction<S> | null | false,
41
41
  /** called before the element is detached */
42
- onUnmount?: MountFunction<S>,
42
+ onUnmount?: MountFunction<S> | null | false,
43
43
  /** used instead of original vode when an error occurs during rendering */
44
44
  catch?: ((s: S, error: Error) => ChildVode<S>) | ChildVode<S>;
45
45
  };
@@ -93,9 +93,8 @@ export interface ContainerNode<S = PatchableState> extends HTMLElement {
93
93
  renderAsync: () => Promise<unknown>,
94
94
  syncRenderer: (cb: () => void) => void,
95
95
  asyncRenderer: ((cb: () => void) => ViewTransition) | null | undefined,
96
- qSync: {} | undefined | null, // next patch aggregate to be applied
97
96
  qAsync: {} | undefined | null, // next render-patches to be animated after another
98
- isRendering: boolean,
97
+ isRendering: number,
99
98
  isAnimating: boolean,
100
99
  /** stats about the overall patches & last render time */
101
100
  stats: {
@@ -146,7 +145,7 @@ export function app<S extends PatchableState = PatchableState>(
146
145
  const _vode = {} as ContainerNode<S>["_vode"];
147
146
  _vode.syncRenderer = globals.requestAnimationFrame;
148
147
  _vode.asyncRenderer = globals.startViewTransition;
149
- _vode.qSync = null;
148
+ _vode.isRendering = 0;
150
149
  _vode.qAsync = null;
151
150
  _vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
152
151
 
@@ -156,68 +155,79 @@ export function app<S extends PatchableState = PatchableState>(
156
155
  initialPatches = [...(state as any).patch.initialPatches, ...initialPatches];
157
156
  }
158
157
 
159
- Object.defineProperty(state, "patch", {
160
- enumerable: false, configurable: true,
161
- writable: false, value: async (action: Patch<S>, isAsync?: boolean) => {
162
- if (!action || (typeof action !== "function" && typeof action !== "object")) return;
163
- _vode.stats.patchCount++;
158
+ async function promisePatch(action: Promise<Patch<S>>, isAnimated?: boolean) {
159
+ _vode.stats.liveEffectCount++;
160
+ try {
161
+ const resolvedPatch = await (action as Promise<S>);
162
+ patchableState.patch(<Patch<S>>resolvedPatch, isAnimated);
163
+ } finally {
164
+ _vode.stats.liveEffectCount--;
165
+ }
166
+ }
164
167
 
165
- if ((action as AsyncGenerator<Patch<S>>)?.next) {
166
- const generator = action as AsyncGenerator<Patch<S>>;
168
+ async function generatorPatch(action: AsyncGenerator<Patch<S>>, isAnimated?: boolean) {
169
+ const generator = action as AsyncGenerator<Patch<S>>;
170
+ _vode.stats.liveEffectCount++;
171
+ try {
172
+ let v = await generator.next();
173
+ while (v.done === false) {
167
174
  _vode.stats.liveEffectCount++;
168
175
  try {
169
- let v = await generator.next();
170
- while (v.done === false) {
171
- _vode.stats.liveEffectCount++;
172
- try {
173
- patchableState.patch(v.value, isAsync);
174
- v = await generator.next();
175
- } finally {
176
- _vode.stats.liveEffectCount--;
177
- }
178
- }
179
- patchableState.patch(v.value as Patch<S>, isAsync);
176
+ patchableState.patch(v.value, isAnimated);
177
+ v = await generator.next();
180
178
  } finally {
181
179
  _vode.stats.liveEffectCount--;
182
180
  }
181
+ }
182
+ patchableState.patch(v.value as Patch<S>, isAnimated);
183
+ } finally {
184
+ _vode.stats.liveEffectCount--;
185
+ }
186
+ }
187
+
188
+ Object.defineProperty(state, "patch", {
189
+ enumerable: false, configurable: true,
190
+ writable: false, value: (action: Patch<S>, isAnimated?: boolean) => {
191
+ while (typeof action === "function") {
192
+ action = (<(s: S) => unknown>action)(_vode.state);
193
+ }
194
+
195
+ if (!action || typeof action !== "object") return;
196
+
197
+ _vode.stats.patchCount++;
198
+
199
+ if ((action as AsyncGenerator<Patch<S>>)?.next) {
200
+ generatorPatch(action as AsyncGenerator<Patch<S>>, isAnimated);
183
201
  } else if ((action as Promise<S>).then) {
184
- _vode.stats.liveEffectCount++;
185
- try {
186
- const resolvedPatch = await (action as Promise<S>);
187
- patchableState.patch(<Patch<S>>resolvedPatch, isAsync);
188
- } finally {
189
- _vode.stats.liveEffectCount--;
190
- }
202
+ promisePatch(action as Promise<S>, isAnimated);
191
203
  } else if (Array.isArray(action)) {
192
204
  if (action.length > 0) {
193
205
  for (const p of action) {
194
206
  patchableState.patch(p, !document.hidden && !!_vode.asyncRenderer);
195
207
  }
196
208
  } else { //when [] is patched: 1. skip current animation 2. merge all queued async patches into synced queue
197
- _vode.qSync = mergeState(_vode.qSync || {}, _vode.qAsync, false);
209
+ mergeState(_vode.state, _vode.qAsync, true);
198
210
  _vode.qAsync = null;
199
211
  try { globals.currentViewTransition?.skipTransition(); } catch { }
200
212
  _vode.stats.syncRenderPatchCount++;
201
213
  _vode.renderSync();
202
214
  }
203
- } else if (typeof action === "function") {
204
- patchableState.patch((<(s: S) => unknown>action)(_vode.state), isAsync);
205
215
  } else {
206
- if (isAsync) {
216
+ if (isAnimated) {
207
217
  _vode.stats.asyncRenderPatchCount++;
208
218
  _vode.qAsync = mergeState(_vode.qAsync || {}, action, false);
209
- await _vode.renderAsync();
219
+ _vode.renderAsync();
210
220
  } else {
211
221
  _vode.stats.syncRenderPatchCount++;
212
- _vode.qSync = mergeState(_vode.qSync || {}, action, false);
222
+ mergeState(_vode.state, action, true);
213
223
  _vode.renderSync();
214
224
  }
215
225
  }
216
226
  }
217
227
  });
218
228
 
219
- function renderDom(isAsync: boolean) {
220
- const sw = Date.now();
229
+ function renderDom(isAnimated: boolean) {
230
+ const sw = performance.now();
221
231
  const vom = dom(_vode.state);
222
232
  _vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom)!;
223
233
 
@@ -226,11 +236,13 @@ export function app<S extends PatchableState = PatchableState>(
226
236
  (<ContainerNode<S>>container)._vode = _vode
227
237
  }
228
238
 
229
- if (!isAsync) {
230
- _vode.stats.lastSyncRenderTime = Date.now() - sw;
239
+ if (!isAnimated) {
240
+ _vode.stats.lastSyncRenderTime = performance.now() - sw;
241
+ const changesSinceRender = _vode.isRendering !== _vode.stats.syncRenderPatchCount;
231
242
  _vode.stats.syncRenderCount++;
232
- _vode.isRendering = false;
233
- if (_vode.qSync) _vode.renderSync();
243
+ _vode.isRendering = 0;
244
+ if (changesSinceRender)
245
+ _vode.renderSync();
234
246
  }
235
247
  }
236
248
  const sr = renderDom.bind(null, false);
@@ -239,13 +251,9 @@ export function app<S extends PatchableState = PatchableState>(
239
251
  Object.defineProperty(_vode, "renderSync", {
240
252
  enumerable: false, configurable: true,
241
253
  writable: false, value: () => {
242
- if (_vode.isRendering || !_vode.qSync) return;
243
-
244
- _vode.isRendering = true;
245
-
246
- _vode.state = mergeState(_vode.state, _vode.qSync, true);
247
- _vode.qSync = null;
254
+ if (_vode.isRendering) return;
248
255
 
256
+ _vode.isRendering = _vode.stats.syncRenderPatchCount;
249
257
  _vode.syncRenderer(sr);
250
258
  }
251
259
  });
@@ -258,7 +266,7 @@ export function app<S extends PatchableState = PatchableState>(
258
266
  if (_vode.isAnimating || !_vode.qAsync || document.hidden) return;
259
267
 
260
268
  _vode.isAnimating = true;
261
- const sw = Date.now();
269
+ const sw = performance.now();
262
270
  try {
263
271
  _vode.state = mergeState(_vode.state, _vode.qAsync, true);
264
272
  _vode.qAsync = null;
@@ -267,7 +275,7 @@ export function app<S extends PatchableState = PatchableState>(
267
275
 
268
276
  await globals.currentViewTransition?.updateCallbackDone;
269
277
  } finally {
270
- _vode.stats.lastAsyncRenderTime = Date.now() - sw;
278
+ _vode.stats.lastAsyncRenderTime = performance.now() - sw;
271
279
  _vode.stats.asyncRenderCount++;
272
280
  _vode.isAnimating = false;
273
281
  }
@@ -281,7 +289,8 @@ export function app<S extends PatchableState = PatchableState>(
281
289
  root._vode = _vode;
282
290
  const indexInParent = Array.from(container.parentElement.children).indexOf(container);
283
291
 
284
- _vode.isRendering = true;
292
+ const patchCountBefore = _vode.stats.syncRenderPatchCount;
293
+ _vode.isRendering = _vode.stats.syncRenderPatchCount;
285
294
  _vode.vode = render(
286
295
  <S>state,
287
296
  container.parentElement,
@@ -290,8 +299,10 @@ export function app<S extends PatchableState = PatchableState>(
290
299
  hydrate<S>(container, true) as AttachedVode<S>,
291
300
  dom(<S>state)
292
301
  )!;
293
- _vode.isRendering = false;
294
- if (_vode.qSync) _vode.renderSync();
302
+ const continueRendering = _vode.stats.syncRenderPatchCount !== patchCountBefore;
303
+ _vode.isRendering = 0;
304
+ _vode.stats.syncRenderCount++;
305
+ if (continueRendering) _vode.renderSync();
295
306
 
296
307
  for (const effect of initialPatches) {
297
308
  patchableState.patch(effect);
@@ -384,13 +395,20 @@ export function hydrate<S = PatchableState>(element: Element | Text, prepareForR
384
395
  }
385
396
 
386
397
  /** memoizes the resulting component or props by comparing element by element (===) with the
387
- * `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)*/
388
- export function memo<S = PatchableState>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
398
+ * `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)
399
+ */
400
+ export function memo<S = PatchableState>(compare: any[], component: Component<S>): Component<S> {
389
401
  if (!compare || !Array.isArray(compare)) throw new Error("first argument to memo() must be an array of values to compare");
390
- if (typeof componentOrProps !== "function") throw new Error("second argument to memo() must be a function that returns a vode or props object");
402
+ if (typeof component !== "function") throw new Error("second argument to memo() must be a function that returns a child vode");
403
+
404
+ if ((<any>component).__memo) { // wrap to prevent double memoization
405
+ const comp = component;
406
+ component = (s: S) => comp(s);
407
+ }
408
+
409
+ (<any>component).__memo = compare;
391
410
 
392
- (<any>componentOrProps).__memo = compare;
393
- return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
411
+ return component;
394
412
  }
395
413
 
396
414
  /**
@@ -559,7 +577,7 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
559
577
  for (let i = indexInParent; i < parent.childNodes.length; i++) {
560
578
  const nextSibling = parent.childNodes[i];
561
579
  if (nextSibling) {
562
- nextSibling.before(text, nextSibling);
580
+ nextSibling.before(text);
563
581
  inserted = true;
564
582
  break;
565
583
  }
@@ -605,7 +623,7 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
605
623
  for (let i = indexInParent; i < parent.childNodes.length; i++) {
606
624
  const nextSibling = parent.childNodes[i];
607
625
  if (nextSibling) {
608
- nextSibling.before(newNode, nextSibling);
626
+ nextSibling.before(newNode);
609
627
  inserted = true;
610
628
  break;
611
629
  }
@@ -615,12 +633,12 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
615
633
  }
616
634
  }
617
635
 
618
- const newKids = children(newVode);
619
- if (newKids) {
636
+ const newStart = childrenStart(newVode);
637
+ if (newStart > 0) {
620
638
  const childOffset = !!properties ? 2 : 1;
621
639
  let indexP = 0;
622
- for (let i = 0; i < newKids.length; i++) {
623
- const child = newKids[i];
640
+ for (let i = 0; i < (<Vode<S>>newVode).length - newStart; i++) {
641
+ const child = (<Vode<S>>newVode)[i + newStart] as ChildVode<S>;
624
642
  // render child in xml mode to prevent using the dom properties
625
643
  const attached = render(state, newNode as Element, i, indexP, undefined, child, xmlns ?? null);
626
644
  (<Vode<S>>newVode!)[i + childOffset] = <Vode<S>>attached;
@@ -639,49 +657,37 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
639
657
  if (!oldIsText && isNode && (<Vode<S>>oldVode)[0] === (<Vode<S>>newVode)[0]) {
640
658
  (<AttachedVode<S>>newVode).node = oldNode;
641
659
 
642
- const newvode = <Vode<S>>newVode;
643
- const oldvode = <Vode<S>>oldVode;
644
-
645
660
  const properties = props(newVode);
646
661
  const oldProps = props(oldVode);
647
662
 
648
- if (properties?.xmlns !== undefined) xmlns = properties.xmlns;
663
+ if (properties?.xmlns !== undefined)
664
+ xmlns = properties.xmlns;
649
665
 
650
- if ((<any>newvode[1])?.__memo) {
651
- const prev = newvode[1] as any;
652
- newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
653
- if (prev !== newvode[1]) {
654
- patchProperties(state, oldNode!, oldProps, properties, xmlns);
655
- }
656
- }
657
- else {
658
- patchProperties(state, oldNode!, oldProps, properties, xmlns);
659
- }
666
+ patchProperties(state, oldNode!, oldProps, properties, xmlns);
660
667
 
661
668
  if (!!properties?.catch && oldProps?.catch !== properties.catch) {
662
669
  (<any>newVode).node['catch'] = null;
663
670
  (<any>newVode).node.removeAttribute('catch');
664
671
  }
665
672
 
666
- const newKids = children(newVode);
667
- const oldKids = children(oldVode) as AttachedVode<S>[];
668
- if (newKids) {
669
- const childOffset = !!properties ? 2 : 1;
673
+ const newStart = childrenStart(newVode);
674
+ const oldStart = childrenStart(oldVode);
675
+ if (newStart > 0) {
670
676
  let indexP = 0;
671
- for (let i = 0; i < newKids.length; i++) {
672
- const child = newKids[i];
673
- const oldChild = oldKids && oldKids[i];
677
+ for (let i = 0; i < (<Vode<S>>newVode).length - newStart; i++) {
678
+ const child = (<Vode<S>>newVode)[i + newStart] as ChildVode<S>;
679
+ const oldChild = oldStart > 0 ? (<Vode<S>>oldVode)[i + oldStart] as AttachedVode<S> : undefined;
674
680
 
675
681
  const attached = render(state, oldNode as Element, i, indexP, oldChild, child, xmlns);
676
- (<Vode<S>>newVode)[i + childOffset] = <Vode<S>>attached;
682
+ (<any>newVode)[i + newStart] = attached;
677
683
  if (attached) indexP++;
678
684
  }
679
685
  }
680
686
 
681
- if (oldKids) {
682
- const newKidsCount = newKids ? newKids.length : 0;
683
- for (let i = oldKids.length - 1; i >= newKidsCount; i--) {
684
- render(state, oldNode as Element, i, i, oldKids[i], undefined, xmlns);
687
+ if (oldStart > 0) {
688
+ const newKidsCount = newStart > 0 ? (<Vode<S>>newVode).length - newStart : 0;
689
+ for (let i = (<Vode<S>>oldVode).length - 1 - oldStart; i >= newKidsCount; i--) {
690
+ render(state, oldNode as Element, i, i, (<any>oldVode)[i + oldStart], undefined, xmlns);
685
691
  }
686
692
  }
687
693
 
@@ -745,14 +751,19 @@ function isTextVode<S>(x: ChildVode<S>): x is TextVode {
745
751
  }
746
752
 
747
753
  function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
754
+ while (typeof present === "function" && !present.__memo) {
755
+ present = present(state);
756
+ }
757
+
748
758
  if (typeof present !== "function")
749
759
  return present;
750
760
 
751
- const presentMemo = present?.__memo;
752
- const pastMemo = past?.__memo;
753
761
 
754
- if (Array.isArray(presentMemo)
755
- && Array.isArray(pastMemo)
762
+ const presentMemo: unknown[] = present?.__memo;
763
+ const pastMemo: unknown[] = past?.__memo;
764
+
765
+ if (
766
+ Array.isArray(presentMemo) && Array.isArray(pastMemo)
756
767
  && presentMemo.length === pastMemo.length
757
768
  ) {
758
769
  let same = true;
@@ -765,40 +776,17 @@ function remember<S>(state: S, present: any, past: any): ChildVode<S> | Attached
765
776
  if (same) return past;
766
777
  }
767
778
 
768
- const result = present(state);
769
-
770
- if (typeof result === "function" && result?.__memo) {
771
- const resultMemo = result.__memo;
772
- if (Array.isArray(resultMemo) && Array.isArray(pastMemo) && resultMemo.length === pastMemo.length) {
773
- let same = true;
774
- for (let i = 0; i < resultMemo.length; i++) {
775
- if (resultMemo[i] !== pastMemo[i]) {
776
- same = false;
777
- break;
778
- }
779
- }
780
- if (same) return past;
781
- }
782
- const innerRender = result(state);
783
- if (typeof innerRender === "object") {
784
- innerRender.__memo = resultMemo;
785
- }
786
- return innerRender;
779
+ // memos are not equal so we unwrap the present
780
+ while (typeof present === "function") {
781
+ present = present(state);
787
782
  }
788
783
 
789
- const newRender = typeof result === "function" ? unwrap(result, state) : result;
790
- if (typeof newRender === "object") {
791
- (<any>newRender).__memo = result?.__memo || present?.__memo;
784
+ // attach memo to the unwrapped present for future comparisons
785
+ if (typeof present === "object") {
786
+ (<any>present).__memo = presentMemo;
792
787
  }
793
- return newRender;
794
- }
795
788
 
796
- function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
797
- if (typeof c === "function") {
798
- return unwrap(c(s), s);
799
- } else {
800
- return c;
801
- }
789
+ return present;
802
790
  }
803
791
 
804
792
  function patchProperties<S extends PatchableState>(