Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Import useMemo and cache loopedPaths and indices to avoid recreating arrays on every render. Update the effect to depend on the memoized loopedPaths so animations update reliably when paths change.
Implement a step prop that switches the component to controlled mode and animates directly to the target step (preserving mid- animation state). Add a useSvgMorph hook to manage step state (setStep/next/prev/isFirst/isLast), include a controlled demo, and update docs and registry metadata to advertise the new mode and hook.
matiasperz
left a comment
There was a problem hiding this comment.
I think this api should be controlled by default instead of including code on it's core to auto-reproduce.
| const currentPathRef = useRef(paths[step]) | ||
| const interpolatorRef = useRef<FlubberInterpolator | null>(null) | ||
|
|
||
| const d = useTransform(progress, (v: number) => { | ||
| if (!interpolatorRef.current) return currentPathRef.current | ||
| return interpolatorRef.current(v) | ||
| }) | ||
|
|
||
| useEffect(() => { | ||
| const targetPath = paths[step] |
There was a problem hiding this comment.
Out-of-bounds step causes a runtime crash
When step >= paths.length, paths[step] is undefined. This means currentPathRef is initialized with undefined (line 70), and the effect's targetPath also becomes undefined (line 79). Passing undefined into flubber.interpolate on line 89 will throw a runtime error.
The useSvgMorph hook correctly clamps step, but a user in controlled mode who passes step directly without the hook (e.g. from external state) can easily trigger this crash — especially when paths arrays across different morph slots have different lengths or when step is derived from dynamic data.
Consider adding a guard at the top of the effect:
useEffect(() => {
const targetPath = paths[step]
if (targetPath === undefined || targetPath === currentPathRef.current) return
// ... rest of the effect
}, [step, duration, progress, paths])And similarly protect the useRef initialization:
const currentPathRef = useRef(paths[Math.min(step, paths.length - 1)] ?? '')Prompt To Fix With AI
This is a comment left during a code review.
Path: registry/joyco/blocks/svg-morph.tsx
Line: 70-79
Comment:
**Out-of-bounds `step` causes a runtime crash**
When `step >= paths.length`, `paths[step]` is `undefined`. This means `currentPathRef` is initialized with `undefined` (line 70), and the effect's `targetPath` also becomes `undefined` (line 79). Passing `undefined` into `flubber.interpolate` on line 89 will throw a runtime error.
The `useSvgMorph` hook correctly clamps `step`, but a user in controlled mode who passes `step` directly without the hook (e.g. from external state) can easily trigger this crash — especially when `paths` arrays across different morph slots have different lengths or when `step` is derived from dynamic data.
Consider adding a guard at the top of the effect:
```tsx
useEffect(() => {
const targetPath = paths[step]
if (targetPath === undefined || targetPath === currentPathRef.current) return
// ... rest of the effect
}, [step, duration, progress, paths])
```
And similarly protect the `useRef` initialization:
```tsx
const currentPathRef = useRef(paths[Math.min(step, paths.length - 1)] ?? '')
```
How can I resolve this? If you propose a fix, please make it concise.| const loopedPaths = useMemo(() => [...paths, paths[0]], [paths]) | ||
| const indices = useMemo(() => loopedPaths.map((_, i) => i), [loopedPaths]) | ||
| const d = useTransform(progress, indices, loopedPaths, { | ||
| mixer: (a, b) => interpolate(a, b, { maxSegmentLength: 20 }), | ||
| }) |
There was a problem hiding this comment.
Empty paths array causes an infinite re-render loop
When paths = [], paths[0] is undefined, so loopedPaths becomes [undefined] and indices becomes [0]. The useEffect then runs animate(progress, 0, ...), which immediately completes (zero displacement). In onComplete, the condition pathIndex === loopedPaths.length - 1 (i.e. 0 === 0) is always true, so it calls progress.set(0) and setPathIndex(1). The next effect run calls animate(progress, 1, ...), which completes and calls setPathIndex(2), and so on — pathIndex increments indefinitely, freezing the UI.
Add an early-return guard at the top of AutoMorphPath:
function AutoMorphPath({ paths, duration, gap, fill }) {
if (paths.length === 0) return null
// ... rest of the component
}Prompt To Fix With AI
This is a comment left during a code review.
Path: registry/joyco/blocks/svg-morph.tsx
Line: 31-35
Comment:
**Empty `paths` array causes an infinite re-render loop**
When `paths = []`, `paths[0]` is `undefined`, so `loopedPaths` becomes `[undefined]` and `indices` becomes `[0]`. The `useEffect` then runs `animate(progress, 0, ...)`, which immediately completes (zero displacement). In `onComplete`, the condition `pathIndex === loopedPaths.length - 1` (i.e. `0 === 0`) is always `true`, so it calls `progress.set(0)` and `setPathIndex(1)`. The next effect run calls `animate(progress, 1, ...)`, which completes and calls `setPathIndex(2)`, and so on — `pathIndex` increments indefinitely, freezing the UI.
Add an early-return guard at the top of `AutoMorphPath`:
```tsx
function AutoMorphPath({ paths, duration, gap, fill }) {
if (paths.length === 0) return null
// ... rest of the component
}
```
How can I resolve this? If you propose a fix, please make it concise.
Result
New
svg-morphregistry component that morphs SVG pathdattributes with auto-looping. Supports multiple morph slots (each cycling through its own path states), static paths for compound shapes, and atransformprop for coordinate system conversion.registry/joyco/blocks/svg-morph.tsx— uses flubber for path interpolation + Motion for animationInstall via:
Dependencies added automatically:
flubber,motion.Media
Screen.Recording.2026-02-12.at.11.24.49.PM.mov
Greptile Summary
This PR adds a new
svg-morphregistry component that smoothly morphs SVG pathdattributes usingflubberfor interpolation andmotionfor animation. The implementation is split cleanly into anAutoMorphPath(self-looping) and aControlledMorphPath(externally driven by astepprop), along with a companionuseSvgMorphhook. The mid-animation interruption handling inControlledMorphPathis a thoughtful touch. Documentation is thorough.Two bugs were found in the component:
step: InControlledMorphPath, ifstep >= paths.length,paths[step]resolves toundefined. Both theuseRefinitialization and theuseEffectcan then passundefinedintoflubber.interpolate, throwing a runtime error. TheuseSvgMorphhook guards against this internally, but users in controlled mode who passstepdirectly are exposed.paths: InAutoMorphPath, passing an emptypathsarray causesloopedPaths = [undefined]. TheonCompletecallback always satisfiespathIndex === loopedPaths.length - 1, sosetPathIndexincrements unboundedly, freezing the UI. A simpleif (paths.length === 0) return nullguard resolves this.Confidence Score: 3/5
steptoControlledMorphPaththrows immediately via flubber, and passing an emptypathsarray toAutoMorphPathproduces an unbounded re-render loop. Both are straightforward to fix with short guard clauses.registry/joyco/blocks/svg-morph.tsx— specifically theAutoMorphPathempty-array edge case (line 31) and theControlledMorphPathout-of-bounds step handling (lines 70 and 79).Important Files Changed
Last reviewed commit: 3d93fba