File tree Expand file tree Collapse file tree
packages/core/src/runtime Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1+ ---
2+ ' @prefresh/core ' : patch
3+ ---
4+
5+ Prevent stale component vnodes from being retained across rerenders and unmounts
Original file line number Diff line number Diff line change 11import { options } from 'preact' ;
2- import { vnodesForComponent } from './vnodesForComponent' ;
2+ import { vnodesForComponent , clearLastSeen } from './vnodesForComponent' ;
33
44const oldUnmount = options . unmount ;
55options . unmount = vnode => {
66 const type = ( vnode || { } ) . type ;
7- if ( typeof type === 'function' && vnodesForComponent . has ( type ) ) {
8- const vnodes = vnodesForComponent . get ( type ) ;
9- if ( vnodes ) {
10- const index = vnodes . indexOf ( vnode ) ;
11- if ( index !== - 1 ) {
12- vnodes . splice ( index , 1 ) ;
7+ if ( typeof type === 'function' ) {
8+ clearLastSeen ( vnode ) ;
9+
10+ if ( vnodesForComponent . has ( type ) ) {
11+ const vnodes = vnodesForComponent . get ( type ) ;
12+ if ( vnodes ) {
13+ const index = vnodes . indexOf ( vnode ) ;
14+ if ( index !== - 1 ) {
15+ vnodes . splice ( index , 1 ) ;
16+ }
1317 }
1418 }
1519 }
Original file line number Diff line number Diff line change @@ -2,7 +2,7 @@ import { options } from 'preact';
22import {
33 vnodesForComponent ,
44 mappedVNodes ,
5- lastSeen ,
5+ setLastSeen ,
66} from './vnodesForComponent' ;
77import { VNODE_COMPONENT } from '../constants' ;
88
@@ -57,7 +57,7 @@ const oldDiffed = options.diffed;
5757options . diffed = vnode => {
5858 if ( vnode && typeof vnode . type === 'function' ) {
5959 const vnodes = vnodesForComponent . get ( vnode . type ) ;
60- lastSeen . set ( vnode . __v , vnode ) ;
60+ setLastSeen ( vnode ) ;
6161 if ( vnodes ) {
6262 const matchingDom = vnodes . filter ( p => p . __c === vnode . __c ) ;
6363 if ( matchingDom . length > 1 ) {
Original file line number Diff line number Diff line change 22export const vnodesForComponent = new WeakMap ( ) ;
33export const mappedVNodes = new WeakMap ( ) ;
44export const lastSeen = new Map ( ) ;
5+
6+ const lastSeenByInstance = new WeakMap ( ) ;
7+
8+ export const setLastSeen = vnode => {
9+ const component = vnode . __c ;
10+ if ( component ) {
11+ if ( lastSeenByInstance . has ( component ) ) {
12+ lastSeen . delete ( lastSeenByInstance . get ( component ) ) ;
13+ }
14+ lastSeenByInstance . set ( component , vnode . __v ) ;
15+ }
16+
17+ lastSeen . set ( vnode . __v , vnode ) ;
18+ } ;
19+
20+ export const clearLastSeen = vnode => {
21+ lastSeen . delete ( vnode . __v ) ;
22+
23+ const component = vnode . __c ;
24+ if ( component && lastSeenByInstance . has ( component ) ) {
25+ lastSeen . delete ( lastSeenByInstance . get ( component ) ) ;
26+ lastSeenByInstance . delete ( component ) ;
27+ }
28+ } ;
Original file line number Diff line number Diff line change 11import { h } from 'preact' ;
2+ import { useState } from 'preact/hooks' ;
23import { useCounter } from './useCounter' ;
34import { StoreProvider } from './context' ;
45import { Products } from './products' ;
@@ -10,6 +11,9 @@ import { Style } from './styles';
1011
1112setup ( h ) ;
1213
14+ const lastSeenPayloads = [ ] ;
15+ self . __lastSeenPayloads = lastSeenPayloads ;
16+
1317function Test ( ) {
1418 const [ count , increment ] = useCounter ( ) ;
1519 return (
@@ -20,10 +24,30 @@ function Test() {
2024 )
2125}
2226
27+ function LastSeenChild ( { payload } ) {
28+ return < span className = "last-seen-child" > { payload . count } </ span > ;
29+ }
30+
31+ function LastSeenTest ( ) {
32+ const [ count , setCount ] = useState ( 0 ) ;
33+ const payload = { count } ;
34+ lastSeenPayloads . push ( new WeakRef ( payload ) ) ;
35+
36+ return (
37+ < div >
38+ < button className = "last-seen-increment" onClick = { ( ) => setCount ( count + 1 ) } >
39+ Increment tracked component
40+ </ button >
41+ < LastSeenChild payload = { payload } />
42+ </ div >
43+ ) ;
44+ }
45+
2346export function App ( props ) {
2447 return (
2548 < Style id = "color" >
2649 < Test />
50+ < LastSeenTest />
2751 < Greeting />
2852 < StoreProvider >
2953 < Products />
Original file line number Diff line number Diff line change 11import { h } from 'preact' ;
2+ import { useState } from 'preact/hooks' ;
23import { useCounter } from './useCounter' ;
34import { StoreProvider } from './context' ;
45import { Products } from './products' ;
@@ -10,6 +11,9 @@ import { Style } from './styles';
1011
1112setup ( h ) ;
1213
14+ const lastSeenPayloads = [ ] ;
15+ self . __lastSeenPayloads = lastSeenPayloads ;
16+
1317function Test ( ) {
1418 const [ count , increment ] = useCounter ( ) ;
1519 return (
@@ -20,10 +24,30 @@ function Test() {
2024 )
2125}
2226
27+ function LastSeenChild ( { payload } ) {
28+ return < span className = "last-seen-child" > { payload . count } </ span > ;
29+ }
30+
31+ function LastSeenTest ( ) {
32+ const [ count , setCount ] = useState ( 0 ) ;
33+ const payload = { count } ;
34+ lastSeenPayloads . push ( new WeakRef ( payload ) ) ;
35+
36+ return (
37+ < div >
38+ < button className = "last-seen-increment" onClick = { ( ) => setCount ( count + 1 ) } >
39+ Increment tracked component
40+ </ button >
41+ < LastSeenChild payload = { payload } />
42+ </ div >
43+ ) ;
44+ }
45+
2346export function App ( props ) {
2447 return (
2548 < Style id = "color" >
2649 < Test />
50+ < LastSeenTest />
2751 < Greeting />
2852 < StoreProvider >
2953 < Products />
Original file line number Diff line number Diff line change @@ -127,6 +127,29 @@ describe('Prefresh integrations', () => {
127127 await expectByPolling ( ( ) => getText ( button ) , 'Increment (+)' ) ;
128128 } ) ;
129129
130+ test ( 'does not retain stale vnodes after rerenders' , async ( ) => {
131+ const increment = await page . $ ( '.last-seen-increment' ) ;
132+
133+ await page . evaluate ( ( ) => {
134+ self . __lastSeenPayloads . length = 0 ;
135+ } ) ;
136+
137+ for ( let i = 0 ; i < 20 ; i ++ ) {
138+ await increment . click ( ) ;
139+ }
140+
141+ await expectByPolling ( ( ) => getText ( '.last-seen-child' ) , '20' ) ;
142+
143+ const client = await page . target ( ) . createCDPSession ( ) ;
144+ await client . send ( 'HeapProfiler.collectGarbage' ) ;
145+ expect (
146+ await page . evaluate (
147+ ( ) => self . __lastSeenPayloads . filter ( ref => ref . deref ( ) ) . length
148+ )
149+ ) . toBe ( 1 ) ;
150+ await client . detach ( ) ;
151+ } ) ;
152+
130153 test ( 'add export' , async ( ) => {
131154 const button = await page . $ ( '.button' ) ;
132155 await expectByPolling ( ( ) => getText ( button ) , 'Increment (+)' ) ;
You can’t perform that action at this time.
0 commit comments