Skip to content

VNode leak using Context #4909

@korDen

Description

@korDen
  • Check if updating to the latest Preact version resolves the issue

Describe the bug
This is a followup to another leak: VNode leak in Context · Issue #4905 · preactjs/preact. There is a VNode memory leak occurring when using Context Provider with state updates. After clicking a button that triggers a state change, the previous VNode hierarchy is still retained in memory, which can be observed in Chrome Dev Tools. This results in duplicate VNodes including 2 <button>s, 2 <App>s, 2 <Context>s, 2 <RouterContent>s and 2 Text nodes (both the old and new text content).

To Reproduce

StackBlitz reproduction link: https://stackblitz.com/edit/vitejs-vite-21mvd37v?file=src%2Fmain.tsx,index.html

Repro:

import { render, createContext } from 'preact';
import { useState, useContext } from 'preact/hooks';

const RouterContext = createContext(null);

function RouterContent() {
  const { params, setRouterState } = useContext(RouterContext);
  return (
    <button onClick={() => setRouterState({ params: params + "!" })}>
      {`No account found for username "${params}"`}
    </button>
  );
}

function App() {
  const [routerState, setRouterState] = useState({ params: 'a' });

  const contextValue = { 
    params: routerState.params,
    setRouterState
  };

  return (
    <RouterContext.Provider value={contextValue}>
      <RouterContent />
    </RouterContext.Provider>
  );
}

render(<App />, document.getElementById('app'));

which compiles to (sometime its easier to check correctness of .js vs .ts):

import { jsx as _jsx } from "preact/jsx-runtime";
import { render, createContext } from 'preact';
import { useState, useContext } from 'preact/hooks';
const RouterContext = createContext(null);
function RouterContent() {
    const { params, setRouterState } = useContext(RouterContext);
    return (_jsx("button", { onClick: () => setRouterState({ params: params + "!" }), children: `No account found for username "${params}"` }));
}
function App() {
    const [routerState, setRouterState] = useState({ params: 'a' });
    const contextValue = {
        params: routerState.params,
        setRouterState
    };
    return (_jsx(RouterContext.Provider, { value: contextValue, children: _jsx(RouterContent, {}) }));
}
render(_jsx(App, {}), document.getElementById('app'));

Steps to reproduce the behavior:

  1. Run the web app (preferably with the previous memory leak patch applied -- I tested with the patch applied).
  2. Open Chrome Dev Tools and navigate to the Components tab (React DevTools extension)
  3. Click on the button that says No account found for username "a"
  4. Observe in Dev Tools that previous VNode hierarchy is still retained alongside the new one
  5. See duplicate components and text nodes indicating a memory leak

Expected behavior
When the state updates and the component re-renders, the previous VNode hierarchy should be properly cleaned up and garbage collected. Only the current VNode tree should exist in memory, not duplicates of all components and text nodes from previous renders.

Image Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions