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", diff --git a/packages/react-wrappers/src/utils.ts b/packages/react-wrappers/src/utils.ts index 3f6c038..4fb1f03 100644 --- a/packages/react-wrappers/src/utils.ts +++ b/packages/react-wrappers/src/utils.ts @@ -104,10 +104,29 @@ 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` : ""} +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 ( @@ -126,19 +145,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 +199,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; `; 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"},