Skip to content

Commit 76854e6

Browse files
committed
Refactor SVG component props and instance handling
Replaces SVGProps with a stricter ComponentProps type, enforces either 'src' or 'content' as required, and refactors instance management to use useRef instead of useState. Updates main.ts to remove SVGProps export. Improves group addition/removal logic and event handler registration for better reliability.
1 parent 2f24b00 commit 76854e6

File tree

2 files changed

+64
-67
lines changed

2 files changed

+64
-67
lines changed

lib/SVG.tsx

Lines changed: 63 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
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';
73
import { Context, useTwo } from './Context';
84

95
import 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

4744
export 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,

lib/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export { Provider as Canvas } from './Provider';
22
export { Context, useTwo, useFrame } from './Context';
33
export { Group, type RefGroup } from './Group';
4-
export { SVG, type RefSVG, type SVGProps } from './SVG';
4+
export { SVG, type RefSVG } from './SVG';
55
export { Path, type RefPath } from './Path';
66
export { Points, type RefPoints } from './Points';
77
export { Text, type RefText } from './Text';

0 commit comments

Comments
 (0)