1- import React , {
2- useEffect ,
3- useImperativeHandle ,
4- useMemo ,
5- useState ,
6- } from 'react' ;
1+ import React , { useEffect , useImperativeHandle , useMemo , useRef } from 'react' ;
2+ import Two from 'two.js' ;
73import { Context , useTwo } from './Context' ;
84
95import type { Group as Instance } from 'two.js/src/group' ;
@@ -23,30 +19,31 @@ type GroupProps =
2319 | 'curved'
2420 | 'automatic' ;
2521
26- export interface SVGProps
27- extends React . PropsWithChildren < {
28- [ K in Extract < GroupProps , keyof Instance > ] ?: Instance [ K ] ;
29- } > ,
30- Partial < EventHandlers > {
31- // Source (one required)
32- src ?: string ; // URL to .svg file
33- content ?: string ; // Inline SVG markup string
34-
35- // Positioning & Transform
36- x ?: number ;
37- y ?: number ;
38-
39- // Callbacks
40- onLoad ?: ( group : RefSVG , svg : SVGElement | SVGElement [ ] ) => void ;
41- onError ?: ( error : Error ) => void ;
42-
43- // Loading behavior
44- shallow ?: boolean ; // Flatten groups when interpreting
45- }
22+ type ComponentProps = React . PropsWithChildren <
23+ {
24+ [ K in Extract < GroupProps , keyof Instance > ] ?: Instance [ K ] ;
25+ } & (
26+ | {
27+ src : string ; // URL to .svg file
28+ content : never ; // Inline SVG markup string
29+ }
30+ | {
31+ // Source (one required)
32+ src : never ; // URL to .svg file
33+ content : string ; // Inline SVG markup string
34+ }
35+ ) & {
36+ x ?: number ;
37+ y ?: number ;
38+ onLoad ?: ( group : Instance , svg : SVGElement | SVGElement [ ] ) => void ;
39+ onError ?: ( error : Error ) => void ;
40+ shallow ?: boolean ; // Flatten groups when interpreting
41+ } & Partial < EventHandlers >
42+ > ;
4643
4744export type RefSVG = Instance ;
4845
49- export const SVG = React . forwardRef < RefSVG , SVGProps > (
46+ export const SVG = React . forwardRef < Instance , ComponentProps > (
5047 ( { x, y, src, content, onLoad, onError, ...props } , forwardedRef ) => {
5148 const {
5249 two,
@@ -56,7 +53,8 @@ export const SVG = React.forwardRef<RefSVG, SVGProps>(
5653 registerEventShape,
5754 unregisterEventShape,
5855 } = useTwo ( ) ;
59- const [ ref , set ] = useState < Instance | null > ( null ) ;
56+ const svg = useMemo ( ( ) => new Two . Group ( ) , [ ] ) ;
57+ const ref = useRef < Instance | null > ( null ) ;
6058
6159 // Extract event handlers from props
6260 const { eventHandlers, shapeProps } = useMemo ( ( ) => {
@@ -78,6 +76,22 @@ export const SVG = React.forwardRef<RefSVG, SVGProps>(
7876 return { eventHandlers, shapeProps } ;
7977 } , [ props ] ) ;
8078
79+ // Hoist instance for async access
80+ useEffect ( ( ) => {
81+ ref . current = svg ;
82+ } , [ svg ] ) ;
83+
84+ // Add group to parent
85+ useEffect ( ( ) => {
86+ if ( parent && svg ) {
87+ parent . add ( svg ) ;
88+
89+ return ( ) => {
90+ parent . remove ( svg ) ;
91+ } ;
92+ }
93+ } , [ svg , parent ] ) ;
94+
8195 // Validate props
8296 useEffect ( ( ) => {
8397 if ( ! src && ! content ) {
@@ -92,7 +106,7 @@ export const SVG = React.forwardRef<RefSVG, SVGProps>(
92106 }
93107 } , [ src , content ] ) ;
94108
95- // Load SVG using two.load()
109+ // Load <svg /> using two.load()
96110 useEffect ( ( ) => {
97111 if ( ! two ) return ;
98112
@@ -104,12 +118,12 @@ export const SVG = React.forwardRef<RefSVG, SVGProps>(
104118 try {
105119 // two.load() returns a Group immediately (empty initially)
106120 // and populates it asynchronously via callback
107- const group = two . load (
121+ two . load (
108122 source ,
109123 ( loadedGroup : Instance , svg : SVGElement | SVGElement [ ] ) => {
110124 if ( ! mounted ) return ;
111125
112- set ( loadedGroup ) ;
126+ ref . current ?. add ( loadedGroup . children ) ;
113127
114128 // Invoke user callback if provided
115129 if ( onLoad ) {
@@ -124,9 +138,6 @@ export const SVG = React.forwardRef<RefSVG, SVGProps>(
124138 }
125139 }
126140 ) ;
127-
128- // Store the group immediately (even though it's empty)
129- set ( group ) ;
130141 } catch ( err ) {
131142 if ( ! mounted ) return ;
132143
@@ -154,56 +165,42 @@ export const SVG = React.forwardRef<RefSVG, SVGProps>(
154165 } ;
155166 } , [ two , src , content , onLoad , onError ] ) ;
156167
157- // Add group to parent
168+ // Update position and properties
158169 useEffect ( ( ) => {
159- if ( parent && ref ) {
160- parent . add ( ref ) ;
170+ // Update position
171+ if ( typeof x === 'number' ) svg . translation . x = x ;
172+ if ( typeof y === 'number' ) svg . translation . y = y ;
161173
162- return ( ) => {
163- parent . remove ( ref ) ;
164- } ;
165- }
166- } , [ ref , parent ] ) ;
174+ const args = { ...shapeProps } ;
175+ delete args . children ; // Allow react to handle children
167176
168- // Update position and properties
169- useEffect ( ( ) => {
170- if ( ref ) {
171- const group = ref ;
172- // Update position
173- if ( typeof x === 'number' ) group . translation . x = x ;
174- if ( typeof y === 'number' ) group . translation . y = y ;
175-
176- const args = { ...shapeProps } ;
177- delete args . children ; // Allow react to handle children
178-
179- // Update other properties (excluding event handlers)
180- for ( const key in args ) {
181- if ( key in group ) {
182- // eslint-disable-next-line @typescript-eslint/no-explicit-any
183- ( group as any ) [ key ] = ( args as any ) [ key ] ;
184- }
177+ // Update other properties (excluding event handlers)
178+ for ( const key in args ) {
179+ if ( key in svg ) {
180+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181+ ( svg as any ) [ key ] = ( args as any ) [ key ] ;
185182 }
186183 }
187- } , [ ref , x , y , shapeProps ] ) ;
184+ } , [ svg , x , y , shapeProps ] ) ;
188185
189186 // Register event handlers
190187 useEffect ( ( ) => {
191- if ( ref && Object . keys ( eventHandlers ) . length > 0 ) {
192- registerEventShape ( ref , eventHandlers , parent ?? undefined ) ;
188+ if ( Object . keys ( eventHandlers ) . length > 0 ) {
189+ registerEventShape ( svg , eventHandlers , parent ?? undefined ) ;
193190
194191 return ( ) => {
195- unregisterEventShape ( ref ) ;
192+ unregisterEventShape ( svg ) ;
196193 } ;
197194 }
198- } , [ ref , registerEventShape , unregisterEventShape , parent , eventHandlers ] ) ;
195+ } , [ svg , registerEventShape , unregisterEventShape , parent , eventHandlers ] ) ;
199196
200- useImperativeHandle ( forwardedRef , ( ) => ref as Instance , [ ref ] ) ;
197+ useImperativeHandle ( forwardedRef , ( ) => svg , [ svg ] ) ;
201198
202199 return (
203200 < Context . Provider
204201 value = { {
205202 two,
206- parent : ref ,
203+ parent : svg ,
207204 width,
208205 height,
209206 registerEventShape,
0 commit comments