From 13e129b79c24655bd77c096132111e4bb4d481c3 Mon Sep 17 00:00:00 2001 From: Burton Smith Date: Sun, 21 Dec 2025 22:22:16 -0500 Subject: [PATCH 1/3] added logic to prevent duplicate vent listners --- packages/react-wrappers/src/utils.ts | 45 +++++++++++++++++++--------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/react-wrappers/src/utils.ts b/packages/react-wrappers/src/utils.ts index 3f6c038..2e8c854 100644 --- a/packages/react-wrappers/src/utils.ts +++ b/packages/react-wrappers/src/utils.ts @@ -104,7 +104,7 @@ export const RESERVED_WORDS = [ export function saveReactUtils(outdir: string, ssrSafe?: boolean) { const reactUtils = ` -import { useEffect, useLayoutEffect } from "react"; +import { useEffect, useLayoutEffect, useRef } from "react"; ${ssrSafe ? `const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect` : ""} @@ -126,19 +126,36 @@ export function useProperties(targetElement, propName, value) { } export function useEventListener(targetElement, eventName, eventHandler) { + // keep a ref to the latest handler so we don't need to re-register the event listener + // whenever the handler changes (avoids duplicate listeners on re-renders) + const handlerRef = useRef(eventHandler); + handlerRef.current = eventHandler; + ${ssrSafe ? "useIsomorphicLayoutEffect" : "useLayoutEffect"}(() => { - if (eventHandler !== undefined) { - targetElement?.current?.addEventListener(eventName, eventHandler); + const element = targetElement?.current; + if (!element || eventName === undefined) { + return; } + // capture the handler at the time the listener is attached so we can call cancel on it + const attachedHandler = handlerRef.current; + + const eventListener = (event) => { + if (handlerRef.current) { + handlerRef.current(event); + } + }; + + element.addEventListener(eventName, eventListener); + return () => { - if (eventHandler?.cancel) { - eventHandler.cancel(); + if (attachedHandler?.cancel) { + attachedHandler.cancel(); } - targetElement?.current?.removeEventListener(eventName, eventHandler); + element.removeEventListener(eventName, eventListener); }; - }, [eventName, eventHandler, targetElement.current]); + }, [eventName, targetElement?.current]); } `; @@ -163,17 +180,17 @@ export function ScopeProvider({ prefix, suffix, children }) { `; const scopeProviderTypes = ` -export type ScopeProps = { +export type ScopeProps = { /** Adds a prefix to the custom element tag name */ - prefix?: string, + prefix?: string, /** Adds a prefix to the custom element tag name */ - suffix?: string, - children?: React.ReactNode + suffix?: string, + children?: React.ReactNode }; -/** - * Provides a mechanism to add a custom prefix or suffix to to child components. - * This prevents tag name collisions with components from different versions of the same library. +/** + * Provides a mechanism to add a custom prefix or suffix to to child components. + * This prevents tag name collisions with components from different versions of the same library. */ export function ScopeProvider(props: ScopeProps): JSX.Element; `; From 8b8fc9f865808214c146fefb8bb24aa85a18d79b Mon Sep 17 00:00:00 2001 From: Burton Smith Date: Sun, 21 Dec 2025 23:59:50 -0500 Subject: [PATCH 2/3] simplify ref handling --- packages/react-wrappers/src/utils.ts | 19 +++++++++++++++++++ .../react-wrappers/src/wrapper-generator.ts | 14 ++++---------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/react-wrappers/src/utils.ts b/packages/react-wrappers/src/utils.ts index 2e8c854..4fb1f03 100644 --- a/packages/react-wrappers/src/utils.ts +++ b/packages/react-wrappers/src/utils.ts @@ -108,6 +108,25 @@ import { useEffect, useLayoutEffect, useRef } from "react"; ${ssrSafe ? `const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect` : ""} +export function mergeRefs(target, forwardedRef) { + if (!forwardedRef) { + return; + } + + if (typeof forwardedRef === "function") { + forwardedRef(target); + } else { + forwardedRef.current = target; + } +} + +export function createForwardedRefHandler(localRef, forwardedRef) { + return (node) => { + localRef.current = node; + mergeRefs(node, forwardedRef); + }; +} + export function useProperties(targetElement, propName, value) { useEffect(() => { if ( diff --git a/packages/react-wrappers/src/wrapper-generator.ts b/packages/react-wrappers/src/wrapper-generator.ts index 5cfc227..785e3c4 100644 --- a/packages/react-wrappers/src/wrapper-generator.ts +++ b/packages/react-wrappers/src/wrapper-generator.ts @@ -337,10 +337,11 @@ function getReactComponentTemplate( } ${config.scopedTags ? ", useContext" : ""} } from "react"; ${!config.ssrSafe ? `import '${modulePath}';` : ""} ${ - has(eventTemplates) || has(propTemplates) + useEffect ? `import { ${has(eventTemplates) ? "useEventListener," : ""} - ${has(propTemplates) ? "useProperties" : ""} + ${has(propTemplates) ? "useProperties," : ""} + createForwardedRefHandler } from './react-utils.js';` : "" } @@ -385,14 +386,7 @@ function getReactComponentTemplate( { ${ useEffect - ? `ref: (node) => { - ref.current = node; - if (typeof forwardedRef === "function") { - forwardedRef(node); - } else if (forwardedRef) { - forwardedRef.current = node; - } - },` + ? `ref: createForwardedRefHandler(ref, forwardedRef),` : "" } ${has(unusedProps) ? "...filteredProps" : "...props"}, From 94ed85f661b1b6ca69bd3efa5a9325732ea0d4fd Mon Sep 17 00:00:00 2001 From: Burton Smith Date: Mon, 22 Dec 2025 00:05:09 -0500 Subject: [PATCH 3/3] prep for release --- packages/react-wrappers/CHANGELOG.md | 5 +++++ packages/react-wrappers/package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-wrappers/CHANGELOG.md b/packages/react-wrappers/CHANGELOG.md index c8add7c..7a8fc96 100644 --- a/packages/react-wrappers/CHANGELOG.md +++ b/packages/react-wrappers/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## 1.7.5 + +- Simplified `ref` handling +- Fixed duplicate event handlers + ## 1.7.4 - Fixed kebab case attribute string mapping fallback diff --git a/packages/react-wrappers/package.json b/packages/react-wrappers/package.json index d29da88..37e4fd9 100644 --- a/packages/react-wrappers/package.json +++ b/packages/react-wrappers/package.json @@ -1,6 +1,6 @@ { "name": "custom-element-react-wrappers", - "version": "1.7.4", + "version": "1.7.5", "description": "A tool for generating react-compatible wrappers for custom elements", "main": "./dist/index.cjs", "module": "./dist/index.js",