Repository: preactjs/preact-devtools
Pull request #255 (2020-09-17 14:31:36)

Add support for filtering HOC-Components
Add support for filtering HOC-Components. This reduces the tree-depth a lot, without sacrificing the information that a HOC is present.

![Screenshot from 2020-09-17 16-29-24](https://user-images.githubusercontent.com/1062408/93485182-16be2680-f903-11ea-8d2b-2c5a3f67b078.png)


**TODO:**

- [x] Add tests for updating in-between HOCs
- [x] ~Initial filter value is not respected on first mount~ - That's a general issue with the filter system. Better to solve in a separate PR.

changeset-bot[bot]: ###  💥  No Changeset

Latest commit: e462ff4bc0d20a49eb023fe918ab7d41706e40e4

Merging this PR will not cause any packages to be released. If these changes should not cause updates to packages in this repo, this is fine 🙂

**If these changes should be published to npm, you need to add a changeset.**



[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/preactjs/preact-devtools/new/hoc?filename=.changeset/weak-lions-try.md&value=---%0A%22%40fake-scope%2Ffake-pkg%22%3A%20patch%0A---%0A%0AAdd%20support%20for%20filtering%20HOC-Components%0A)



Patch:
diff --git a/.changeset/itchy-fishes-hang.md b/.changeset/itchy-fishes-hang.md
new file mode 100644
index 0000000..b522dc3
--- /dev/null
+++ b/.changeset/itchy-fishes-hang.md
@@ -0,0 +1,5 @@
+---
+"preact-devtools": minor
+---
+
+Add support for filtering HOC-Components
diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index f77a860..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,3 +0,0 @@
-[**/*.{js,jsx,ts,tsx,json}]
-indent_style = tab
-tab_width = 2
diff --git a/src/adapter/10/renderer.ts b/src/adapter/10/renderer.ts
index 24acec7..4f91b02 100644
--- a/src/adapter/10/renderer.ts
+++ b/src/adapter/10/renderer.ts
@@ -142,6 +142,24 @@ function updateHighlight(profiler: ProfilerState, vnode: VNode) {
 	}
 }
 
+function getHocName(name: string) {
+	const idx = name.indexOf("(");
+	if (idx === -1) return null;
+
+	const wrapper = name.slice(0, idx);
+	return wrapper ? wrapper : null;
+}
+
+function addHocs(commit: Commit, id: ID, hocs: string[]) {
+	if (hocs.length > 0) {
+		commit.operations.push(MsgTypes.HOC_NODES, id, hocs.length);
+		for (let i = 0; i < hocs.length; i++) {
+			const stringId = getStringId(commit.strings, hocs[i]);
+			commit.operations.push(stringId);
+		}
+	}
+}
+
 export function mount(
 	ids: IdMappingState,
 	commit: Commit,
@@ -151,6 +169,7 @@ export function mount(
 	domCache: WeakMap<HTMLElement | Text, VNode>,
 	config: RendererConfig10,
 	profiler: ProfilerState,
+	hocs: string[],
 ) {
 	if (commit.stats !== null) {
 		commit.stats.mounts++;
@@ -159,36 +178,66 @@ export function mount(
 	const root = isRoot(vnode, config);
 
 	const skip = shouldFilter(vnode, filters, config);
+
 	if (root || !skip) {
-		const id = hasVNodeId(ids, vnode)
-			? getVNodeId(ids, vnode)
-			: createVNodeId(ids, vnode);
-		if (isRoot(vnode, config)) {
-			commit.operations.push(MsgTypes.ADD_ROOT, id);
-		}
+		record: {
+			let name = getDisplayName(vnode, config);
+
+			if (filters.type.has("hoc")) {
+				const hocName = getHocName(name);
+
+				// Filter out HOC-Components
+				if (hocName) {
+					if (name.startsWith("ForwardRef")) {
+						hocs = [...hocs, hocName];
+						const idx = name.indexOf("(");
+						name = name.slice(idx + 1, -1) || "Anonymous";
+					} else {
+						hocs = [...hocs, hocName];
+						break record;
+					}
+				}
+			}
 
-		commit.operations.push(
-			MsgTypes.ADD_VNODE,
-			id,
-			getDevtoolsType(vnode), // Type
-			ancestorId,
-			9999, // owner
-			getStringId(commit.strings, getDisplayName(vnode, config)),
-			vnode.key ? getStringId(commit.strings, vnode.key) : 0,
-			// Multiply, because operations array only supports integers
-			// and would otherwise cut off floats
-			(vnode.startTime || 0) * 1000,
-			(vnode.endTime || 0) * 1000,
-		);
+			const id = hasVNodeId(ids, vnode)
+				? getVNodeId(ids, vnode)
+				: createVNodeId(ids, vnode);
+			if (isRoot(vnode, config)) {
+				commit.operations.push(MsgTypes.ADD_ROOT, id);
+			}
+			commit.operations.push(
+				MsgTypes.ADD_VNODE,
+				id,
+				getDevtoolsType(vnode), // Type
+				ancestorId,
+				9999, // owner
+				getStringId(commit.strings, name),
+				vnode.key ? getStringId(commit.strings, vnode.key) : 0,
+				// Multiply, because operations array only supports integers
+				// and would otherwise cut off floats
+				(vnode.startTime || 0) * 1000,
+				(vnode.endTime || 0) * 1000,
+			);
 
-		// Capture render reason (mount here)
-		if (profiler.isProfiling && profiler.captureRenderReasons) {
-			commit.operations.push(MsgTypes.RENDER_REASON, id, RenderReason.MOUNT, 0);
-		}
+			if (hocs.length > 0) {
+				addHocs(commit, id, hocs);
+				hocs = [];
+			}
+
+			// Capture render reason (mount here)
+			if (profiler.isProfiling && profiler.captureRenderReasons) {
+				commit.operations.push(
+					MsgTypes.RENDER_REASON,
+					id,
+					RenderReason.MOUNT,
+					0,
+				);
+			}
 
-		updateHighlight(profiler, vnode);
+			updateHighlight(profiler, vnode);
 
-		ancestorId = id;
+			ancestorId = id;
+		}
 	}
 
 	if (skip && typeof vnode.type !== "function") {
@@ -217,6 +266,7 @@ export function mount(
 				domCache,
 				config,
 				profiler,
+				hocs,
 			);
 		}
 	}
@@ -258,6 +308,7 @@ export function update(
 	domCache: WeakMap<HTMLElement | Text, VNode>,
 	config: RendererConfig10,
 	profiler: ProfilerState,
+	hocs: string[],
 ) {
 	if (commit.stats !== null) {
 		commit.stats.updates++;
@@ -286,6 +337,7 @@ export function update(
 					domCache,
 					config,
 					profiler,
+					hocs,
 				);
 			}
 		}
@@ -298,7 +350,17 @@ export function update(
 	}
 
 	if (!hasVNodeId(ids, vnode)) {
-		mount(ids, commit, vnode, ancestorId, filters, domCache, config, profiler);
+		mount(
+			ids,
+			commit,
+			vnode,
+			ancestorId,
+			filters,
+			domCache,
+			config,
+			profiler,
+			hocs,
+		);
 		return true;
 	}
 
@@ -310,6 +372,15 @@ export function update(
 		(vnode.endTime || 0) * 1000,
 	);
 
+	const name = getDisplayName(vnode, config);
+	const hoc = getHocName(name);
+	if (hoc) {
+		hocs = [...hocs, hoc];
+	} else {
+		addHocs(commit, id, hocs);
+		hocs = [];
+	}
+
 	const oldVNode = getVNodeById(ids, id);
 	updateVNodeId(ids, id, vnode);
 
@@ -347,7 +418,7 @@ export function update(
 				diff = getDiffType(child, diff);
 				childCount++;
 			}
-			update(ids, commit, child, id, filters, domCache, config, profiler);
+			update(ids, commit, child, id, filters, domCache, config, profiler, hocs);
 			// TODO: This is only sometimes necessary
 			shouldReorder = true;
 		} else {
@@ -355,7 +426,7 @@ export function update(
 				diff = getDiffType(child, diff);
 				childCount++;
 			}
-			mount(ids, commit, child, id, filters, domCache, config, profiler);
+			mount(ids, commit, child, id, filters, domCache, config, profiler, hocs);
 			shouldReorder = true;
 		}
 	}
@@ -407,9 +478,29 @@ export function createCommit(
 	}
 
 	if (isNew) {
-		mount(ids, commit, vnode, parentId, filters, domCache, config, profiler);
+		mount(
+			ids,
+			commit,
+			vnode,
+			parentId,
+			filters,
+			domCache,
+			config,
+			profiler,
+			[],
+		);
 	} else {
-		update(ids, commit, vnode, parentId, filters, domCache, config, profiler);
+		update(
+			ids,
+			commit,
+			vnode,
+			parentId,
+			filters,
+			domCache,
+			config,
+			profiler,
+			[],
+		);
 	}
 
 	commit.rootId = getVNodeId(ids, vnode);
@@ -419,7 +510,7 @@ export function createCommit(
 
 const DEFAULT_FIlTERS: FilterState = {
 	regex: [],
-	type: new Set(["dom", "fragment"]),
+	type: new Set(["dom", "fragment", "hoc"]),
 };
 
 export interface Preact10Renderer extends Renderer {
@@ -563,7 +654,7 @@ export function createRenderer(
 			const vnode = domToVNode.get(node);
 			if (vnode) {
 				if (shouldFilter(vnode, filters, config)) {
-					let p: VNode | null = vnode;
+					let p = vnode;
 					let found = null;
 					while ((p = getVNodeParent(p)) != null) {
 						if (!shouldFilter(p, filters, config)) {
@@ -699,9 +790,7 @@ export function createRenderer(
 				const c = getComponent(vnode);
 				if (c) {
 					const s = getHookState(c, index);
-					// Only useState and useReducer hooks marked as editable so state can
-					// cast to more specific ReducerHookState value.
-					(s as [any, any])[0] = value;
+					s[0] = value;
 					c.forceUpdate();
 				}
 			}
diff --git a/src/adapter/10/renderer/renderReasons.ts b/src/adapter/10/renderer/renderReasons.ts
index 947ce0a..d698a58 100644
--- a/src/adapter/10/renderer/renderReasons.ts
+++ b/src/adapter/10/renderer/renderReasons.ts
@@ -5,8 +5,6 @@ import {
 	getStatefulHookValue,
 	isUseReducerOrState,
 	getVNodeParent,
-	getStartTime,
-	getEndTime,
 } from "../vnode";
 import { ID } from "../../../view/store/types";
 
@@ -111,8 +109,8 @@ export function getRenderReason(
 	const parent = getVNodeParent(next);
 	if (
 		parent != null &&
-		getStartTime(next) >= getStartTime(parent) &&
-		getEndTime(next) <= getEndTime(parent)
+		(next.startTime || 0) >= parent.startTime &&
+		(next.endTime || 0) <= parent.endTime
 	) {
 		return createReason(RenderReason.PARENT_UPDATE, null);
 	}
diff --git a/src/adapter/10/vnode.ts b/src/adapter/10/vnode.ts
index 2da9e83..9dd08ab 100644
--- a/src/adapter/10/vnode.ts
+++ b/src/adapter/10/vnode.ts
@@ -1,25 +1,15 @@
+import { Component, VNode } from "preact";
 import { RendererConfig10 } from "./renderer";
 import { HookType } from "../../constants";
 
-import type { Component, VNode } from "preact";
-import type {
-	Component as IComponent,
-	VNode as IVNode,
-} from "preact/src/internal";
-
-// These hook types are declared in "preact/hooks/src/internal" but not very
-// complete, so for now loosely declare locally.
-type ComponentHooks = Record<string, any>;
-type HookState = Record<string, any>;
-
 // Mangle accessors
 
 /**
  * Get the direct parent of a `vnode`
  */
-export function getVNodeParent(vnode: VNode): VNode | null {
+export function getVNodeParent(vnode: VNode) {
 	return (
-		(vnode as IVNode)._parent ||
+		(vnode as any)._parent ||
 		(vnode as any).__ ||
 		// Older Preact X versions used `__p`
 		(vnode as any).__p ||
@@ -30,32 +20,32 @@ export function getVNodeParent(vnode: VNode): VNode | null {
 /**
  * Check if a `vnode` is the root of a tree
  */
-export function isRoot(vnode: VNode, config: RendererConfig10): boolean {
+export function isRoot(vnode: VNode, config: RendererConfig10) {
 	return getVNodeParent(vnode) == null && vnode.type === config.Fragment;
 }
 
 /**
- * Return the component instance of a `vnode` or `hookState`
+ * Return the component instance of a `vnode`
  */
-export function getComponent(node: HookState | VNode): Component | null {
-	return (node as HookState | IVNode)._component || (node as any).__c || null;
+export function getComponent(vnode: VNode | any): Component | null {
+	return (vnode as any)._component || (vnode as any).__c || null;
 }
 
 /**
  * Get a `vnode`'s _dom reference.
  */
 export function getDom(vnode: VNode): HTMLElement | Text | null {
-	return (vnode as IVNode)._dom || (vnode as any).__e || null;
+	return (vnode as any)._dom || (vnode as any).__e || null;
 }
 
-export function hasDom(x: any): boolean {
+export function hasDom(x: any) {
 	return x != null && ("_dom" in x || "__e" in x);
 }
 
 /**
  * Check if a `vnode` represents a `Suspense` component
  */
-export function isSuspenseVNode(vnode: VNode): boolean {
+export function isSuspenseVNode(vnode: VNode) {
 	const c = getComponent(vnode) as any;
 	// FIXME: Mangling of `_childDidSuspend` is not stable in Preact
 	return c != null && c._childDidSuspend;
@@ -64,11 +54,11 @@ export function isSuspenseVNode(vnode: VNode): boolean {
 /**
  * Get the internal hooks state of a component
  */
-export function getComponentHooks(c: Component): ComponentHooks | null {
+export function getComponentHooks(c: Component) {
 	return (c as any).__hooks || (c as any).__H || null;
 }
 
-export function getStatefulHooks(c: Component): HookState[] | null {
+export function getStatefulHooks(c: Component) {
 	const hooks = getComponentHooks(c);
 	return hooks !== null
 		? hooks._list ||
@@ -78,11 +68,11 @@ export function getStatefulHooks(c: Component): HookState[] | null {
 		: null;
 }
 
-export function isUseReducerOrState(hookState: HookState): boolean {
+export function isUseReducerOrState(hookState: any) {
 	return !!hookState._component || !!hookState.__c;
 }
 
-export function getStatefulHookValue(hookState: HookState): unknown {
+export function getStatefulHookValue(hookState: any) {
 	if (hookState !== null) {
 		const value = hookState._value || hookState.__ || null;
 		if (value !== null && Array.isArray(value)) {
@@ -93,11 +83,7 @@ export function getStatefulHookValue(hookState: HookState): unknown {
 	return null;
 }
 
-export function getHookState(
-	c: Component,
-	index: number,
-	type?: HookType,
-): unknown {
+export function getHookState(c: Component, index: number, type?: HookType) {
 	const list = getStatefulHooks(c);
 	if (list && list[index]) {
 		// useContext
@@ -123,12 +109,12 @@ export function getHookState(
 }
 
 /**
- * Get the diffed children of a `vnode`
+ * Get teh diffed children of a `vnode`
  */
 export function getActualChildren(
 	vnode: VNode,
 ): Array<VNode | null | undefined> {
-	return (vnode as IVNode)._children || (vnode as any).__k || [];
+	return (vnode as any)._children || (vnode as any).__k || [];
 }
 
 // End Mangle accessors
@@ -150,7 +136,7 @@ export function findRoot(vnode: VNode, config: RendererConfig10): VNode {
 /**
  * Get the ancestor component that rendered the current vnode
  */
-export function getAncestor(vnode: VNode): VNode | null {
+export function getAncestor(vnode: VNode) {
 	let next: VNode | null = vnode;
 	while ((next = getVNodeParent(next)) != null) {
 		return next;
@@ -162,7 +148,7 @@ export function getAncestor(vnode: VNode): VNode | null {
 /**
  * Get human readable name of the component/dom element
  */
-export function getDisplayName(vnode: VNode, config: RendererConfig10): string {
+export function getDisplayName(vnode: VNode, config: RendererConfig10) {
 	const { type } = vnode;
 	if (type === config.Fragment) return "Fragment";
 	else if (typeof type === "function") {
@@ -192,18 +178,18 @@ export function getDisplayName(vnode: VNode, config: RendererConfig10): string {
 	return "#text";
 }
 
-export function getEndTime(vnode: VNode): number {
+export function getEndTime(vnode: VNode) {
 	return vnode.endTime || 0;
 }
 
-export function getStartTime(vnode: VNode): number {
+export function getStartTime(vnode: VNode) {
 	return vnode.startTime || 0;
 }
 
-export function getNextState<S>(c: Component): S {
-	return (c as IComponent)._nextState || (c as any).__s || null;
+export function getNextState(c: Component) {
+	return (c as any)._nextState || (c as any).__s || null;
 }
 
-export function setNextState<S>(c: Component, value: S): S {
-	return ((c as IComponent)._nextState = (c as any).__s = value);
+export function setNextState(c: Component, value: any) {
+	return ((c as any)._nextState = (c as any).__s = value);
 }
diff --git a/src/adapter/adapter/filter.ts b/src/adapter/adapter/filter.ts
index 5d3b06d..ef0cbec 100644
--- a/src/adapter/adapter/filter.ts
+++ b/src/adapter/adapter/filter.ts
@@ -5,10 +5,11 @@ export interface RawFilterState {
 	type: {
 		fragment: boolean;
 		dom: boolean;
+		hoc?: boolean;
 	};
 }
 
-export type TypeFilterValue = "dom" | "fragment";
+export type TypeFilterValue = "dom" | "fragment" | "hoc";
 
 export interface FilterState {
 	regex: RegExp[];
@@ -19,6 +20,7 @@ export function parseFilters(raw: RawFilterState): FilterState {
 	const type = new Set<TypeFilterValue>();
 	if (raw.type.fragment) type.add("fragment");
 	if (raw.type.dom) type.add("dom");
+	if (raw.type.hoc) type.add("hoc");
 
 	return {
 		regex: raw.regex.filter(x => x.enabled).map(x => new RegExp(x.value, "gi")),
diff --git a/src/adapter/events/events.ts b/src/adapter/events/events.ts
index d0a8c44..9e56f59 100644
--- a/src/adapter/events/events.ts
+++ b/src/adapter/events/events.ts
@@ -13,6 +13,7 @@ export enum MsgTypes {
 	REORDER_CHILDREN = 5,
 	RENDER_REASON = 6,
 	COMMIT_STATS = 7,
+	HOC_NODES = 8,
 }
 
 // Event Examples:
@@ -65,6 +66,11 @@ export enum MsgTypes {
 //
 // COMMIT_STATS -> Check `stats.ts`
 //
+// HOC_NODES
+//  vnodeId
+//  stringsCounts
+//  ...stringIds
+//
 export interface Commit {
 	rootId: number;
 	strings: StringTable;
diff --git a/src/adapter/events/legacy/operationsV1.ts b/src/adapter/events/legacy/operationsV1.ts
index bef821c..7e9f8ae 100644
--- a/src/adapter/events/legacy/operationsV1.ts
+++ b/src/adapter/events/legacy/operationsV1.ts
@@ -49,6 +49,7 @@ export function applyOperationsV1(store: Store, data: number[]) {
 						children: [],
 						depth,
 						id,
+						hocs: null,
 						name,
 						parent: parentId,
 						type,
diff --git a/src/adapter/events/operations.ts b/src/adapter/events/operations.ts
index 01bbb0b..40782a1 100644
--- a/src/adapter/events/operations.ts
+++ b/src/adapter/events/operations.ts
@@ -43,6 +43,7 @@ export function ops2Tree(oldTree: Tree, existingRoots: ID[], ops: number[]) {
 					children: [],
 					depth: parent ? parent.depth + 1 : 0,
 					id,
+					hocs: null,
 					name: strings[ops[i + 5] - 1],
 					parent: parentId,
 					type: ops[i + 2],
@@ -139,6 +140,20 @@ export function ops2Tree(oldTree: Tree, existingRoots: ID[], ops: number[]) {
 				i = i + 1 + count;
 				break;
 			}
+			case MsgTypes.HOC_NODES: {
+				const vnodeId = ops[i + 1];
+				const vnode = pending.get(vnodeId);
+				const count = ops[i + 2];
+				if (vnode) {
+					const hocs = [];
+					for (let j = 0; j < count; j++) {
+						hocs.push(strings[ops[i + 3 + j] - 1]);
+					}
+					vnode.hocs = hocs;
+				}
+				i = i + 2 + count;
+				break;
+			}
 			default:
 				throw new Error("Unknown event: " + ops[i]);
 		}
diff --git a/src/view/components/Devtools.css b/src/view/components/Devtools.css
index 6692dd4..b34bf52 100644
--- a/src/view/components/Devtools.css
+++ b/src/view/components/Devtools.css
@@ -44,6 +44,7 @@ input {
 :root {
 	--font-monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo,
 		Courier, monospace;
+	--font-xxxsmall: 0.5625rem; /* 9px */
 	--font-xxsmall: 0.625rem; /* 10px */
 	--font-xsmall: 0.6875rem; /* 11px */
 	--font-small: 0.75rem; /* 12px */
@@ -92,6 +93,10 @@ input {
 	--color-key-value-selected: #ff5722;
 	--color-radio: #7245c7;
 	--color-shadow: rgba(0, 0, 0, 0.15);
+	--color-hoc-bg: #ddd;
+	--color-hoc-text: #111;
+	--color-hoc-selected-text: #fff;
+	--color-hoc-selected-bg: #4101b3;
 
 	/* Table */
 	--table-hover: rgba(174, 128, 255, 0.4);
@@ -177,6 +182,10 @@ input {
 	--color-key-value-selected: yellow;
 	--color-radio: #d2adff;
 	--color-shadow: rgba(255, 255, 255, 0.15);
+	--color-hoc-bg: #505050;
+	--color-hoc-text: #fff;
+	--color-hoc-selected-text: #fff;
+	--color-hoc-selected-bg: #8251d8;
 
 	/* Table */
 	--table-hover: rgba(174, 128, 255, 0.4);
diff --git a/src/view/components/elements/TreeBar.tsx b/src/view/components/elements/TreeBar.tsx
index 630afb7..84b5607 100644
--- a/src/view/components/elements/TreeBar.tsx
+++ b/src/view/components/elements/TreeBar.tsx
@@ -67,7 +67,7 @@ export function TreeBar() {
 					onInput={e => store.search.onChange((e.target as any).value)}
 				/>
 				{searchActive && (
-					<div class={s.searchCounter}>
+					<div class={s.searchCounter} data-testid="search-counter">
 						{count > 0 ? selected + 1 : 0} | {count}
 					</div>
 				)}
@@ -124,6 +124,7 @@ export function FilterPopup() {
 	const store = useStore();
 	const filters = useObserver(() => store.filter.filters.$);
 	const filterDom = useObserver(() => store.filter.filterDom.$);
+	const filterHoc = useObserver(() => store.filter.filterHoc.$);
 	const filterFragment = useObserver(() => store.filter.filterFragment.$);
 
 	return (
@@ -148,6 +149,19 @@ export function FilterPopup() {
 					</span>
 					<span class={s.filterValue}>Fragments</span>
 				</label>
+				<label class={s.filterRow}>
+					<span class={s.filterCheck}>
+						<input
+							type="checkbox"
+							checked={filterHoc}
+							onInput={e =>
+								store.filter.setEnabled("hoc", (e.target as any).checked)
+							}
+						/>
+						{filterHoc ? <CheckboxChecked /> : <CheckboxUnChecked />}
+					</span>
+					<span class={s.filterValue}>HOC-Components</span>
+				</label>
 				<label class={s.filterRow}>
 					<span class={s.filterCheck}>
 						<input
diff --git a/src/view/components/elements/TreeView.css b/src/view/components/elements/TreeView.css
index 5967197..ccc6514 100644
--- a/src/view/components/elements/TreeView.css
+++ b/src/view/components/elements/TreeView.css
@@ -68,7 +68,7 @@
 	font-size: 0.9rem;
 	font-family: var(--font-monospace);
 	color: var(--color-element-text);
-	height: 1.25rem;
+	height: 1.125rem;
 	position: absolute;
 	width: 100%;
 	left: 0;
@@ -80,6 +80,11 @@
 	color: var(--color-selected-text);
 }
 
+.tree:focus-within .item[data-selected] :global(.hoc-item) {
+	background: var(--color-hoc-selected-bg);
+	color: var(--color-hoc-selected-text);
+}
+
 .tree:focus-within .item[data-selected]::after {
 	background: var(--color-selected-bg);
 }
@@ -185,3 +190,11 @@
 .markSelected {
 	background: var(--color-marker-selected);
 }
+
+.hocs {
+	margin-left: 0.5rem;
+}
+
+.hocs > * + * {
+	margin-left: 0.25rem;
+}
diff --git a/src/view/components/elements/TreeView.tsx b/src/view/components/elements/TreeView.tsx
index 8a5afd5..cca616c 100644
--- a/src/view/components/elements/TreeView.tsx
+++ b/src/view/components/elements/TreeView.tsx
@@ -14,6 +14,7 @@ import { debounce } from "../../../shells/shared/utils";
 import { EmitFn } from "../../../adapter/hook";
 import { useVirtualizedList } from "./VirtualizedList";
 import { useAutoIndent } from "./useAutoIndent";
+import { Hoc } from "../sidebar/HocPanel";
 
 const ROW_HEIGHT = 18;
 
@@ -229,6 +230,17 @@ export function TreeItem(props: { key: any; id: ID; top: number }) {
 					) : (
 						""
 					)}
+					{node.hocs && node.hocs.length > 0 && (
+						<span class={s.hocs}>
+							{node.hocs.map((hoc, i) => {
+								return (
+									<Hoc key={i} small>
+										<MarkResult text={hoc} id={id} />
+									</Hoc>
+								);
+							})}
+						</span>
+					)}
 				</span>
 			</div>
 		</div>
diff --git a/src/view/components/profiler/flamegraph/modes/patchTree.ts b/src/view/components/profiler/flamegraph/modes/patchTree.ts
index 4351cd2..27e6cca 100644
--- a/src/view/components/profiler/flamegraph/modes/patchTree.ts
+++ b/src/view/components/profiler/flamegraph/modes/patchTree.ts
@@ -29,6 +29,7 @@ function createGroup(): DevNode {
 		type: DevNodeType.Group,
 		children: [],
 		depth: -1,
+		hocs: null,
 		endTime: -1,
 		startTime: -1,
 		name: "__PREACT_GROUP__",
diff --git a/src/view/components/profiler/flamegraph/placeNodes.ts b/src/view/components/profiler/flamegraph/placeNodes.ts
index 3d623ae..00ece1f 100644
--- a/src/view/components/profiler/flamegraph/placeNodes.ts
+++ b/src/view/components/profiler/flamegraph/placeNodes.ts
@@ -46,6 +46,7 @@ export const EMPTY: DevNode = {
 	depth: 0,
 	endTime: 0,
 	id: -1,
+	hocs: null,
 	key: "",
 	name: "",
 	parent: -1,
diff --git a/src/view/components/sidebar/HocPanel.css b/src/view/components/sidebar/HocPanel.css
new file mode 100644
index 0000000..5f017b3
--- /dev/null
+++ b/src/view/components/sidebar/HocPanel.css
@@ -0,0 +1,36 @@
+.root {
+	border-bottom: 0.0625rem solid var(--color-border);
+	padding: 0.1875rem 0.6rem;
+	display: flex;
+	flex-wrap: wrap;
+}
+
+.root .hoc {
+	margin-bottom: 0.2rem;
+	margin-top: 0.2rem;
+}
+
+.root .hoc {
+	margin-right: 0.4rem;
+}
+
+.root:last-of-type(.hoc) {
+	margin-right: 0;
+}
+
+.hoc {
+	display: inline-block;
+	background: var(--color-hoc-bg);
+	border-radius: 0.1875rem;
+	font-size: var(--font-small);
+	font-family: var(--font-monospace);
+	padding: 0.2rem 0.4rem;
+	line-height: normal;
+	color: var(--color-hoc-text);
+}
+
+.hoc.small {
+	font-size: var(--font-xxxsmall);
+	border-radius: 0.125rem;
+	padding: 0.125rem 0.25rem;
+}
diff --git a/src/view/components/sidebar/HocPanel.tsx b/src/view/components/sidebar/HocPanel.tsx
new file mode 100644
index 0000000..c83eea2
--- /dev/null
+++ b/src/view/components/sidebar/HocPanel.tsx
@@ -0,0 +1,24 @@
+import { h } from "preact";
+import s from "./HocPanel.css";
+
+export interface Props {
+	hocs: string[];
+}
+
+export function Hoc(props: { children: any; small?: boolean }) {
+	return (
+		<span class={`${s.hoc} ${props.small ? s.small : ""} hoc-item`}>
+			{props.children}
+		</span>
+	);
+}
+
+export function HocPanel(props: Props) {
+	return (
+		<div class={s.root} data-testid="hoc-panel">
+			{props.hocs.map((hoc, i) => (
+				<Hoc key={i}>{hoc}</Hoc>
+			))}
+		</div>
+	);
+}
diff --git a/src/view/components/sidebar/Sidebar.tsx b/src/view/components/sidebar/Sidebar.tsx
index 8aef211..3326d12 100644
--- a/src/view/components/sidebar/Sidebar.tsx
+++ b/src/view/components/sidebar/Sidebar.tsx
@@ -5,11 +5,19 @@ import { serializeProps } from "./inspect/serializeProps";
 import { DebugTreeStats } from "./DebugTreeStats";
 import { DebugNodeNavTree } from "./DebugNodeNavTree";
 import { KeyPanel } from "./KeyPanel";
+import { HocPanel } from "./HocPanel";
 
 export function Sidebar() {
 	const store = useStore();
 	const showDebug = useObserver(() => store.debugMode.$);
 	const inspect = useObserver(() => store.inspectData.$);
+	const hocs = useObserver(() => {
+		if (store.inspectData.$) {
+			const node = store.nodes.$.get(store.inspectData.$.id);
+			return node ? node.hocs : null;
+		}
+		return null;
+	});
 	const { props: propData, state, context, hooks } = store.sidebar;
 	const { emit } = store;
 
@@ -21,6 +29,7 @@ export function Sidebar() {
 					onCopy={() => emit("copy", inspect.key!)}
 				/>
 			)}
+			{inspect && hocs !== null && hocs.length > 0 && <HocPanel hocs={hocs} />}
 			<PropsPanel
 				label="Props"
 				items={propData.items}
diff --git a/src/view/components/tree/windowing.ts b/src/view/components/tree/windowing.ts
index 0f4b5c6..46ff032 100644
--- a/src/view/components/tree/windowing.ts
+++ b/src/view/components/tree/windowing.ts
@@ -3,7 +3,7 @@ import { ID } from "../../store/types";
 export function flattenChildren<
 	K,
 	T extends { id: K; children: K[]; depth: number }
->(tree: Map<K, T>, id: K, isCollapsed: (id: K) => boolean): { items: K[] } {
+>(tree: Map<K, T>, id: K, isCollapsed: (id: K) => boolean): K[] {
 	const out: K[] = [];
 	const visited = new Set<K>();
 	const stack: K[] = [id];
@@ -27,7 +27,7 @@ export function flattenChildren<
 		}
 	}
 
-	return { items: out };
+	return out;
 }
 
 export function clamp(n: number, max: number) {
diff --git a/src/view/store/filter.ts b/src/view/store/filter.ts
index cfe8065..6bb8fd6 100644
--- a/src/view/store/filter.ts
+++ b/src/view/store/filter.ts
@@ -13,6 +13,7 @@ export function createFilterStore(
 	const filters = valoo<RawFilter[]>([]);
 	const filterFragment = valoo(true);
 	const filterDom = valoo(true);
+	const filterHoc = valoo(true);
 
 	const submit = () => {
 		const s: RawFilterState = {
@@ -20,6 +21,7 @@ export function createFilterStore(
 			type: {
 				fragment: filterFragment.$,
 				dom: filterDom.$,
+				hoc: filterHoc.$,
 			},
 		};
 
@@ -33,7 +35,7 @@ export function createFilterStore(
 		try {
 			filterFragment.$ = !!state.type.fragment;
 			filterDom.$ = !!state.type.dom;
-			filterDom.$ = !!state.type.dom;
+			filterHoc.$ = state.type.hoc !== undefined ? state.type.hoc : true;
 			filters.$ = state.regex;
 		} catch (err) {
 			// eslint-disable-next-line no-console
@@ -45,12 +47,15 @@ export function createFilterStore(
 		filters,
 		filterFragment,
 		filterDom,
+		filterHoc,
 		setEnabled(filter: RawFilter | string, v: boolean) {
 			if (typeof filter === "string") {
 				if (filter === "dom") {
 					filterDom.$ = v;
 				} else if (filter === "fragment") {
 					filterFragment.$ = v;
+				} else if (filter === "hoc") {
+					filterHoc.$ = v;
 				}
 			} else {
 				filter.enabled = v;
diff --git a/src/view/store/index.ts b/src/view/store/index.ts
index e4a4f54..c662ab0 100644
--- a/src/view/store/index.ts
+++ b/src/view/store/index.ts
@@ -32,7 +32,7 @@ export function createStore(): Store {
 
 	const nodeList = watch(() => {
 		return roots.$.map(root => {
-			const { items } = flattenChildren<ID, DevNode>(nodes.$, root, id =>
+			const items = flattenChildren<ID, DevNode>(nodes.$, root, id =>
 				collapser.collapsed.$.has(id),
 			);
 
diff --git a/src/view/store/props.ts b/src/view/store/props.ts
index d1d2b9d..0e5383f 100644
--- a/src/view/store/props.ts
+++ b/src/view/store/props.ts
@@ -30,7 +30,7 @@ export function parseObjectState(
 		});
 
 		parseProps(data, "root", PROPS_LIMIT, 0, "root", tree);
-		const { items } = flattenChildren(tree, "root", id => {
+		const items = flattenChildren(tree, "root", id => {
 			return tree.get(id)!.children.length > 0 && isCollapsed(uncollapsed, id);
 		});
 		return items.slice(1).map(id => tree.get(id)!);
diff --git a/src/view/store/search.ts b/src/view/store/search.ts
index 4715275..2ab4933 100644
--- a/src/view/store/search.ts
+++ b/src/view/store/search.ts
@@ -47,7 +47,10 @@ export function createSearchStore(
 		const ids: number[] = [];
 		list.$.forEach(id => {
 			const node = items.$.get(id);
-			if (node && reg.test(node.name)) {
+			if (
+				node &&
+				(reg.test(node.name) || (node.hocs && node.hocs.some(h => reg.test(h))))
+			) {
 				ids.push(id);
 			}
 		});
diff --git a/src/view/store/types.ts b/src/view/store/types.ts
index e699978..6468c42 100644
--- a/src/view/store/types.ts
+++ b/src/view/store/types.ts
@@ -32,9 +32,11 @@ export interface DevNode {
 	id: ID;
 	type: DevNodeType;
 	name: string;
-	key: string;
+	key: string | null;
 	parent: ID;
 	children: ID[];
+	/** Higher Order Component wrappers */
+	hocs: string[] | null;
 
 	// Display (Elements + Profiler)
 	depth: number;


Test patch:
diff --git a/src/adapter/events/operations.test.ts b/src/adapter/events/operations.test.ts
index 0a93b42..682e12f 100644
--- a/src/adapter/events/operations.test.ts
+++ b/src/adapter/events/operations.test.ts
@@ -41,6 +41,7 @@ describe("ops2Tree", () => {
 					name: "Fragment",
 					parent: -1,
 					type: 3,
+					hocs: null,
 					key: "",
 					startTime: 42,
 					endTime: 42,
@@ -65,6 +66,7 @@ describe("ops2Tree", () => {
 					parent: -1,
 					type: 3,
 					key: "",
+					hocs: null,
 					startTime: 42,
 					endTime: 42,
 				},
@@ -76,6 +78,7 @@ describe("ops2Tree", () => {
 					parent: 1,
 					type: 1,
 					key: "",
+					hocs: null,
 					startTime: 42,
 					endTime: 42,
 				},
diff --git a/src/view/components/elements/TreeView.test.tsx b/src/view/components/elements/TreeView.test.tsx
index a2865bb..84da595 100644
--- a/src/view/components/elements/TreeView.test.tsx
+++ b/src/view/components/elements/TreeView.test.tsx
@@ -15,6 +15,7 @@ describe("TreeItem", () => {
 			endTime: 0,
 			key: "abcdefghijklmnopqrstuvxyz",
 			id: 1,
+			hocs: null,
 			name: "foo",
 			parent: -1,
 			startTime: 0,
diff --git a/src/view/components/profiler/flamegraph/testHelpers.test.ts b/src/view/components/profiler/flamegraph/testHelpers.test.ts
index 031f51b..7d7cad3 100644
--- a/src/view/components/profiler/flamegraph/testHelpers.test.ts
+++ b/src/view/components/profiler/flamegraph/testHelpers.test.ts
@@ -19,6 +19,7 @@ describe("FlameGraph DSL", () => {
 				startTime: 0,
 				endTime: 130,
 				children: [2],
+				hocs: null,
 				parent: -1,
 				depth: 0,
 			},
@@ -30,6 +31,7 @@ describe("FlameGraph DSL", () => {
 				startTime: 10,
 				endTime: 110,
 				children: [3],
+				hocs: null,
 				parent: 1,
 				depth: 1,
 			},
@@ -42,6 +44,7 @@ describe("FlameGraph DSL", () => {
 				endTime: 100,
 				depth: 2,
 				children: [],
+				hocs: null,
 				parent: 2,
 			},
 		];
@@ -65,6 +68,7 @@ describe("FlameGraph DSL", () => {
 				startTime: 0,
 				endTime: 130,
 				children: [2],
+				hocs: null,
 				parent: -1,
 				depth: 0,
 			},
@@ -76,6 +80,7 @@ describe("FlameGraph DSL", () => {
 				startTime: 0,
 				endTime: 100,
 				children: [3],
+				hocs: null,
 				parent: 1,
 				depth: 1,
 			},
@@ -89,6 +94,7 @@ describe("FlameGraph DSL", () => {
 				parent: 2,
 				depth: 2,
 				children: [],
+				hocs: null,
 			},
 		];
 
@@ -151,6 +157,7 @@ describe("FlameGraph DSL", () => {
 				startTime: 0,
 				endTime: 220,
 				children: [2, 3],
+				hocs: null,
 				parent: -1,
 				depth: 0,
 			},
@@ -162,6 +169,7 @@ describe("FlameGraph DSL", () => {
 				startTime: 10,
 				endTime: 110,
 				children: [],
+				hocs: null,
 				parent: 1,
 				depth: 1,
 			},
@@ -174,6 +182,7 @@ describe("FlameGraph DSL", () => {
 				endTime: 200,
 				depth: 1,
 				children: [],
+				hocs: null,
 				parent: 1,
 			},
 		];
@@ -197,6 +206,7 @@ describe("FlameGraph DSL", () => {
 				startTime: 0,
 				endTime: 220,
 				children: [2, 4],
+				hocs: null,
 				depth: 0,
 				parent: -1,
 			},
@@ -208,6 +218,7 @@ describe("FlameGraph DSL", () => {
 				startTime: 10,
 				endTime: 110,
 				children: [3],
+				hocs: null,
 				parent: 1,
 				depth: 1,
 			},
@@ -220,6 +231,7 @@ describe("FlameGraph DSL", () => {
 				endTime: 100,
 				depth: 2,
 				children: [],
+				hocs: null,
 				parent: 2,
 			},
 			{
@@ -231,6 +243,7 @@ describe("FlameGraph DSL", () => {
 				endTime: 200,
 				depth: 1,
 				children: [5],
+				hocs: null,
 				parent: 1,
 			},
 			{
@@ -242,6 +255,7 @@ describe("FlameGraph DSL", () => {
 				endTime: 200,
 				depth: 2,
 				children: [],
+				hocs: null,
 				parent: 4,
 			},
 		];
diff --git a/src/view/components/profiler/flamegraph/testHelpers.ts b/src/view/components/profiler/flamegraph/testHelpers.ts
index 4c5fc35..32d72ed 100644
--- a/src/view/components/profiler/flamegraph/testHelpers.ts
+++ b/src/view/components/profiler/flamegraph/testHelpers.ts
@@ -87,6 +87,7 @@ export function flames(
 				endTime: startTime + duration,
 				children: [],
 				id: id++,
+				hocs: null,
 				name,
 			};
 
diff --git a/src/view/components/tree/windowing.test.ts b/src/view/components/tree/windowing.test.ts
index ac58b2c..13ab7da 100644
--- a/src/view/components/tree/windowing.test.ts
+++ b/src/view/components/tree/windowing.test.ts
@@ -13,8 +13,13 @@ describe("flattenChildren", () => {
 
 	it("should flatten tree", () => {
 		const collapsed = new Set<ID>();
-		expect(
-			flattenChildren(tree, 1, id => collapsed.has(id)).items,
-		).to.deep.equal([1, 2, 4, 3, 5, 6]);
+		expect(flattenChildren(tree, 1, id => collapsed.has(id))).to.deep.equal([
+			1,
+			2,
+			4,
+			3,
+			5,
+			6,
+		]);
 	});
 });
diff --git a/test-e2e/fixtures/hoc-update.js b/test-e2e/fixtures/hoc-update.js
new file mode 100644
index 0000000..39b60ff
--- /dev/null
+++ b/test-e2e/fixtures/hoc-update.js
@@ -0,0 +1,48 @@
+const { render, Component } = preact;
+const { memo, forwardRef } = preactCompat;
+
+function Foo() {
+	return html`<div>I am foo</div>`;
+}
+const MemoFoo = memo(Foo);
+
+function Wrapped(props) {
+	return html`<div>${props.children}</div>`;
+}
+
+const Forward = forwardRef(function Bar(props, ref) {
+	return html`<div ...${props} ref=${ref}>
+		forward
+	</div>`;
+});
+
+function withBoof(Comp) {
+	class Boof extends Component {
+		render() {
+			return html`<div>
+				<button
+					onClick=${() => this.setState(p => ({ condition: !p.condition }))}
+				>
+					Update
+				</button>
+				<${Comp} ...${this.props}
+					>${this.state.condition
+						? html`<${MemoFoo} />`
+						: html`<${Forward} />`}<//
+				>
+			</div>`;
+		}
+	}
+	Boof.displayName = "withBoof(" + (Comp.displayName || Comp.name) + ")";
+	return Boof;
+}
+
+const Custom = withBoof(Wrapped);
+
+render(
+	html`
+		<div><b>Custom HOC</b></div>
+		<${Custom} />
+	`,
+	document.getElementById("app"),
+);
diff --git a/test-e2e/fixtures/hoc.js b/test-e2e/fixtures/hoc.js
new file mode 100644
index 0000000..d5ccc74
--- /dev/null
+++ b/test-e2e/fixtures/hoc.js
@@ -0,0 +1,53 @@
+const { render, Component } = preact;
+const { memo, forwardRef } = preactCompat;
+
+function Foo() {
+	return html`<div>I am foo</div>`;
+}
+const MemoFoo = memo(Foo);
+
+function Last() {
+	return html`<div>I am last</div>`;
+}
+const MemoLast = memo(Last);
+
+const Forward = forwardRef(function Bar(props, ref) {
+	return html`<div ...${props} ref=${ref}>
+		forward
+	</div>`;
+});
+
+const ForwardAnonym = forwardRef((props, ref) => {
+	return html`<div ...${props} ref=${ref}>
+		forward anonymous
+	</div>`;
+});
+
+function withBoof(Comp) {
+	class Boof extends Component {
+		render() {
+			return html`<${Comp} ...${this.props} />`;
+		}
+	}
+	Boof.displayName = "withBoof(" + (Comp.displayName || Comp.name) + ")";
+	return Boof;
+}
+
+const Custom = withBoof(Foo);
+const Multiple = withBoof(MemoLast);
+
+render(
+	html`
+		<div><b>MemoFoo</b></div>
+		<${MemoFoo} />
+		<div><b>ForwardRef</b></div>
+		<${Forward} />
+		<div><b>ForwardRef (Anonymous)</b></div>
+		<${ForwardAnonym} />
+		<div><b>Custom HOC</b></div>
+		<${Custom} />
+		<div><b>Multple HOCs</b></div>
+		<${Multiple} />
+	`,
+	document.getElementById("app"),
+);
diff --git a/test-e2e/fixtures/index.html b/test-e2e/fixtures/index.html
index d0bf765..5aec20a 100644
--- a/test-e2e/fixtures/index.html
+++ b/test-e2e/fixtures/index.html
@@ -52,6 +52,8 @@
 				{ name: "update-all" },
 				{ name: "useCallback" },
 				{ name: "keys" },
+				{ name: "hoc" },
+				{ name: "hoc-update" },
 			];
 
 			const list = document.querySelector("#list");
diff --git a/test-e2e/tests/hoc-filter-disable.test.ts b/test-e2e/tests/hoc-filter-disable.test.ts
new file mode 100644
index 0000000..129d1b5
--- /dev/null
+++ b/test-e2e/tests/hoc-filter-disable.test.ts
@@ -0,0 +1,43 @@
+import { newTestPage } from "../test-utils";
+import { expect } from "chai";
+import {
+	assertNotTestId,
+	clickNestedText,
+	clickTestId,
+	waitForTestId,
+} from "pentf/browser_utils";
+
+export const description = "HOC-Component filter should be disabled";
+
+export async function run(config: any) {
+	const { devtools } = await newTestPage(config, "hoc");
+
+	await clickTestId(devtools, "filter-menu-button");
+	await waitForTestId(devtools, "filter-popup");
+	await clickNestedText(devtools, "HOC-Components");
+	await clickTestId(devtools, "filter-update");
+
+	await devtools.waitForSelector(
+		'[data-testid="tree-item"][data-name="Memo(Foo)"]',
+	);
+
+	const items = await devtools.evaluate(() => {
+		return Array.from(
+			document.querySelectorAll('[data-testid="tree-item"]'),
+		).map(el => el.getAttribute("data-name"));
+	});
+
+	expect(items).to.deep.equal([
+		"Memo(Foo)",
+		"Foo",
+		"ForwardRef(Bar)",
+		"ForwardRef()",
+		"withBoof(Foo)",
+		"Foo",
+		"withBoof(Memo(Last))",
+		"Memo(Last)",
+		"Last",
+	]);
+
+	await assertNotTestId(devtools, "hoc-panel");
+}
diff --git a/test-e2e/tests/hoc-filter-search.test.ts b/test-e2e/tests/hoc-filter-search.test.ts
new file mode 100644
index 0000000..5b5fa66
--- /dev/null
+++ b/test-e2e/tests/hoc-filter-search.test.ts
@@ -0,0 +1,35 @@
+import { newTestPage, typeText } from "../test-utils";
+import { expect } from "chai";
+import { getText } from "pentf/browser_utils";
+import { assertEventually } from "pentf/assert_utils";
+
+export const description = "HOC-Component labels should be searchable";
+export async function run(config: any) {
+	const { devtools } = await newTestPage(config, "hoc");
+
+	await devtools.waitForSelector('[data-testid="tree-item"][data-name="Foo"]');
+	await typeText(devtools, '[data-testid="element-search"]', "forw");
+
+	let marked = await devtools.$$("mark");
+	expect(marked.length).to.equal(2);
+	expect(
+		await marked[0].evaluate(el => el.hasAttribute("data-marked")),
+	).to.equal(true);
+
+	await assertEventually(
+		async () => {
+			const text = await getText(devtools, '[data-testid="search-counter"]');
+			expect(text).to.equal("1 | 2");
+			return true;
+		},
+		{ timeout: 2000, crashOnError: false },
+	);
+
+	await devtools.keyboard.press("Enter");
+
+	marked = await devtools.$$("mark");
+	expect(marked.length).to.equal(2);
+	expect(
+		await marked[1].evaluate(el => el.hasAttribute("data-marked")),
+	).to.equal(true);
+}
diff --git a/test-e2e/tests/hoc-filter-update.test.ts b/test-e2e/tests/hoc-filter-update.test.ts
new file mode 100644
index 0000000..0c9eb1e
--- /dev/null
+++ b/test-e2e/tests/hoc-filter-update.test.ts
@@ -0,0 +1,41 @@
+import { newTestPage } from "../test-utils";
+import { expect } from "chai";
+import { Page } from "puppeteer";
+
+async function getTreeItems(page: Page) {
+	return await page.evaluate(() => {
+		return Array.from(
+			document.querySelectorAll('[data-testid="tree-item"]'),
+		).map(el => {
+			return {
+				name: el.getAttribute("data-name"),
+				hocs: Array.from(el.querySelectorAll(".hoc-item")).map(
+					h => h.textContent,
+				),
+			};
+		});
+	});
+}
+
+export const description = "HOC-Component should work with updates";
+export async function run(config: any) {
+	const { devtools, page } = await newTestPage(config, "hoc-update");
+	await devtools.waitForSelector(
+		'[data-testid="tree-item"][data-name="Wrapped"]',
+	);
+
+	let items = await getTreeItems(devtools);
+	expect(items).to.deep.equal([
+		{ name: "Wrapped", hocs: ["withBoof"] },
+		{ name: "Bar", hocs: ["ForwardRef"] },
+	]);
+
+	// Trigger update
+	await page.click("button");
+
+	items = await getTreeItems(devtools);
+	expect(items).to.deep.equal([
+		{ name: "Wrapped", hocs: ["withBoof"] },
+		{ name: "Foo", hocs: ["Memo"] },
+	]);
+}
diff --git a/test-e2e/tests/hoc-filter.test.ts b/test-e2e/tests/hoc-filter.test.ts
new file mode 100644
index 0000000..584ff73
--- /dev/null
+++ b/test-e2e/tests/hoc-filter.test.ts
@@ -0,0 +1,44 @@
+import { newTestPage } from "../test-utils";
+import { expect } from "chai";
+import { clickNestedText, waitForTestId } from "pentf/browser_utils";
+import { assertEventually } from "pentf/assert_utils";
+
+export const description = "HOC-Component filter should flatten tree";
+
+export async function run(config: any) {
+	const { devtools } = await newTestPage(config, "hoc");
+
+	await devtools.waitForSelector('[data-testid="tree-item"][data-name="Foo"]');
+
+	const items = await devtools.evaluate(() => {
+		return Array.from(
+			document.querySelectorAll('[data-testid="tree-item"]'),
+		).map(el => el.getAttribute("data-name"));
+	});
+
+	expect(items).to.deep.equal(["Foo", "Bar", "Anonymous", "Foo", "Last"]);
+
+	await clickNestedText(devtools, "Anonymous");
+	await waitForTestId(devtools, "hoc-panel");
+
+	async function getSidebarHocs() {
+		return await devtools.evaluate(() => {
+			return Array.from(
+				document.querySelectorAll('[data-testid="hoc-panel"] .hoc-item'),
+			).map(el => el.textContent);
+		});
+	}
+
+	const hocs = await getSidebarHocs();
+	expect(hocs).to.deep.equal(["ForwardRef"]);
+
+	await clickNestedText(devtools, "Last");
+
+	await assertEventually(
+		async () => {
+			const hocs2 = await getSidebarHocs();
+			expect(hocs2).to.deep.equal(["withBoof", "Memo"]);
+		},
+		{ timeout: 2000, crashOnError: false },
+	);
+}