11'use client' ;
2- import { useRef , type RefObject } from 'react' ;
2+ import { useRef } from 'react' ;
33import { cssVar , makeSetter } from './internal.js' ;
44
55type UIAction < T extends string > = T | ( ( prev : T ) => T ) ;
66
7- type ScopedRef = RefObject < HTMLElement | null > | ( ( ( node : HTMLElement | null ) => void ) & { current : HTMLElement | null } ) ;
7+ type ScopedRef = ( ( node : HTMLElement | null ) => void ) & { current : HTMLElement | null } ;
88
99interface ScopedSetterFn < T extends string = string > {
1010 ( action : UIAction < T > ) : void ; // ← SINGLE source of truth
@@ -22,40 +22,38 @@ function useScopedUI<T extends string = string>(key: string, initialValue: T, fl
2222 // Create a ref to hold the DOM element that will receive the data-* attributes
2323 // This allows scoping UI state to specific elements instead of always using document.body
2424 const scopeRef = useRef < HTMLElement | null > ( null ) ;
25-
2625 const setterFn = useRef ( makeSetter ( key , initialValue , ( ) => scopeRef . current ! , flag ) ) . current as ScopedSetterFn < T > ;
27-
28- if ( process . env . NODE_ENV !== 'production' ) {
29- // -- DEV-ONLY MULTIPLE REF GUARD (removed in production by modern bundlers) --
30- // Attach the ref to the setter function so users can write: <div ref={setterFn.ref} />
31- const refAttachCount = useRef ( 0 ) ;
32- // DEV: Wrap scopeRef to detect multiple attachments
33- const attachRef = ( ( node : HTMLElement | null ) => {
34- if ( node ) {
35- refAttachCount ! . current ++ ;
36- if ( refAttachCount ! . current > 1 ) {
37- // TODO add documentation link
38- throw new Error (
39- `[useUI] Multiple ref attachments detected for key "${ key } ". ` +
40- `Each useScopedUI hook supports only one ref attachment per component. ` +
41- `Solution: Create separate component. and reuse.\n` +
42- `React Strict Mode May Cause the Ref to be attached multiple times.`
43- ) ;
26+ const refAttachCount = useRef ( 0 ) ;
27+ const attachRef = useRef < ScopedRef | null > ( null ) ;
28+
29+ if ( ! attachRef . current ) {
30+ attachRef . current = ( ( node : HTMLElement | null ) => {
31+ if ( process . env . NODE_ENV !== 'production' ) {
32+ if ( node ) {
33+ refAttachCount . current ++ ;
34+ if ( refAttachCount . current > 1 ) {
35+ // TODO add documentation link
36+ throw new Error (
37+ `[useUI] Multiple ref attachments detected for key "${ key } ". ` +
38+ `Each useScopedUI hook supports only one ref attachment per component. ` +
39+ `Solution: Create separate component. and reuse.\n` +
40+ `React Strict Mode May Cause the Ref to be attached multiple times.`
41+ ) ;
42+ }
43+ } else {
44+ // Handle cleanup when ref is detached
45+ refAttachCount . current = Math . max ( 0 , refAttachCount . current - 1 ) ;
4446 }
45- } else {
46- // Handle cleanup when ref is detached
47- refAttachCount ! . current = Math . max ( 0 , refAttachCount ! . current - 1 ) ;
4847 }
48+
4949 scopeRef . current = node ;
50- attachRef . current = node ;
51- } ) as ( ( node : HTMLElement | null ) => void ) & { current : HTMLElement | null } ;
52- attachRef . current = null ;
53- ( setterFn as ScopedSetterFn < T > ) . ref = attachRef ;
54- } else {
55- // PROD: Direct ref assignment for zero overhead
56- setterFn . ref = scopeRef ;
50+ attachRef . current ! . current = node ;
51+ } ) as ScopedRef ;
52+ attachRef . current . current = null ;
5753 }
5854
55+ setterFn . ref = attachRef . current ;
56+
5957 // Return tuple matching React's useState pattern: [initialValue, setter]
6058 return [ initialValue , setterFn ] ;
6159}
0 commit comments