Lottie Animation Integration for MotionForge#9
Conversation
- Created <Lottie /> component with deterministic frame synchronization. - Added support for remote JSON URLs and local JSON objects. - Implemented frameStart, frameEnd, playbackRate, and loop props. - Integrated Lottie with Sequence relative frame system. - Updated create-motionforge templates with Lottie support. - Added DemoLottie composition for examples and testing. - Bumped version to 1.3.0 and updated CHANGELOG.md. - Updated README with comprehensive Lottie documentation. - Optimized performance with memoization and instance cleanup. - Ensured SSR compatibility. - Fixed AbsoluteFill type definition to allow optional children. Co-authored-by: codedbytahir <200578194+codedbytahir@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
✅ Deploy Preview for motion-forge ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis PR integrates Lottie animations into MotionForge by adding a new Lottie component with frame-synced playback, lottie-web dependency, demo components, template updates, type refinements, and documentation. Changes
Sequence Diagram(s)sequenceDiagram
participant React as React Component
participant MotionForge as MotionForge Context
participant Lottie as Lottie Component
participant LottieWeb as lottie-web Library
React->>Lottie: Mount with props (src, frameStart, frameEnd)
Lottie->>LottieWeb: Initialize animation (SVG renderer, no autoplay)
LottieWeb-->>Lottie: Animation ready (DOMLoaded/data_ready)
Lottie->>Lottie: Set isLoaded = true
loop On Each Frame
MotionForge->>Lottie: Provide currentFrame (absolute or relative)
Lottie->>Lottie: Compute targetFrame (playbackRate, frameStart/frameEnd, loop)
Lottie->>LottieWeb: goToAndStop(mappedFrame, true)
LottieWeb-->>React: Render frame
end
React->>Lottie: Unmount
Lottie->>LottieWeb: Destroy animation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Fix all issues with AI agents
In `@packages/create-motionforge/templates/hello-world/page.tsx.template`:
- Around line 49-53: The page.tsx.template currently uses an external Lottie URL
in the <Lottie> component which is fragile; replace that external src usage by
bundling an inline Lottie JSON (like the simpleLottie object defined in
DemoLottie.tsx) and pass it to the Lottie component (or import
DemoLottie/simpleLottie) so the starter template renders reliably offline and
matches the demo; update the <Lottie> usage in page.tsx.template to accept the
inlined JSON instead of the hardcoded "https://assets.lottiefiles.com/..." URL
and keep the existing playbackRate and loop props.
In `@packages/create-motionforge/templates/shared/package.json.template`:
- Around line 12-13: Update the package version for the "motionforge" dependency
in package.json.template from "^1.2.0" to "^1.3.0" so it is compatible with the
included "lottie-web" dependency; locate the "motionforge" entry in the shared
package.json.template and bump the version string to "^1.3.0" (ensure the
trailing comma and JSON formatting remain valid).
In `@packages/motionforge/CHANGELOG.md`:
- Line 8: Update the release date in the changelog header for version marker "##
[1.3.0] - 2024-03-20" to the correct release date (replace 2024-03-20 with
2026-02-15) so the header reads "## [1.3.0] - 2026-02-15"; locate the exact
header line containing "## [1.3.0]" in CHANGELOG.md and change only the date
portion.
In `@packages/motionforge/src/components/Lottie.tsx`:
- Around line 143-157: The mapping of currentFrame to Lottie frames can produce
negative targetFrame when playbackRate is negative and the modulo preserves
sign; update the targetFrame calculation in the Lottie component (symbols:
currentFrame, playbackRate, loop, duration, start, targetFrame, finalFrame,
anim.goToAndStop) so that after computing targetFrame = currentFrame *
playbackRate you: for loop=true normalize the modulo to a non-negative value
(e.g. ((targetFrame % duration) + duration) % duration) and for loop=false clamp
to the [0, duration - 0.01] range (use Math.max(0, ...) or equivalent), then
compute finalFrame = start + targetFrame and call anim.goToAndStop(finalFrame,
true).
- Around line 80-125: The effect currently re-initializes whenever the inline
object `src` reference changes, causing infinite re-creates and never clearing
prior errors; update the effect in the component that uses useEffect (the effect
that references `src`, `containerRef`, `animationRef`, `setIsLoaded`, and
`setError`) to compute a stable key for `src` (e.g., use string key = typeof src
=== 'string' ? src : JSON.stringify(src)), track the previous key with a ref and
bail out early if the key hasn't changed, and when the key does change reset
state by calling setError(null) and setIsLoaded(false) before creating the new
lottie instance; then use that stable key (not the raw object) in the effect
dependency array so inline object props no longer force re-initialization.
In `@src/lib/remotion/components/Lottie.tsx`:
- Around line 143-154: The targetFrame calculation can become negative when
relativeFrame/currentFrame is negative; update the mapping in the Lottie
component so targetFrame is clamped to a non-negative value before applying
looping or duration bounds. Specifically, in the block computing targetFrame
(symbols: currentFrame, playbackRate, targetFrame, loop, duration) ensure you
compute targetFrame = Math.max(0, currentFrame * playbackRate) (or equivalent)
and then apply the loop modulo or Math.min clipping, and ensure finalFrame
(symbols: start, finalFrame) cannot end up before start by preserving that
clamp. This keeps defensive safety for edge cases where relativeFrame may be
negative.
- Around line 80-125: When src changes, reset the loading state at the start of
the init effect to avoid stale true values: inside the useEffect that depends on
src (the effect creating the lottie AnimationItem and using animationRef), call
setIsLoaded(false) before initiating load, and also clear any previous setError
if present; ensure the onLoaded callback still sets setIsLoaded(true) when
DOMLoaded/data_ready fire and that cleanup still destroys anim and nulls
animationRef.current to prevent races.
- Line 125: The effect in the Lottie component currently lists src in its
dependency array which causes infinite re-initialization when callers pass an
inline object; update the hook to stabilize the dependency by deriving a
serialized key (e.g. const srcKey = useMemo(() => typeof src === 'string' ? src
: JSON.stringify(src), [src]) or compute a ref-based deep-compare) and use
srcKey in the effect dependency instead of src (or alternatively document that
callers must memoize src); locate the effect in the Lottie component that
currently ends with "}, [src]);" and replace the dependency with the derived
srcKey so the animation is not destroyed/recreated on every render.
In `@src/lib/remotion/components/Sequence.tsx`:
- Around line 110-112: The exported type for useRelativeCurrentFrame is
incorrect in the distributed types; update the declaration so
useRelativeCurrentFrame returns number | null to match the implementation that
uses RelativeFrameContext (createContext<number | null>) and the hook
useRelativeCurrentFrame; find the exported type/signature for
useRelativeCurrentFrame in the generated declaration file (the dist/index.d.ts
export for useRelativeCurrentFrame) and change its return type from number to
number | null so external consumers see the nullable return.
🧹 Nitpick comments (6)
packages/motionforge/package.json (1)
76-78: Consider makinglottie-weba peer or optional dependency.
lottie-webis a sizable library (~250KB+ minified). As a harddependency, it's always installed even for users who never use the<Lottie />component. Making it apeerDependency(oroptionalDependency) would let consumers opt in, keeping the default install leaner.♻️ Suggested change
- "dependencies": { - "lottie-web": "^5.13.0" - }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0", + "lottie-web": ">=5.12.0" + }, + "peerDependenciesMeta": { + "lottie-web": { + "optional": true + } + },packages/motionforge/src/components/Lottie.tsx (2)
121-124: SynchronoussetErrorinuseEffectcatch block (ESLintreact-hooks/set-state-in-effect).The ESLint hint is valid —
setErroris called synchronously in the effect body (not in a callback/event). While this particular case (inside acatch) is unlikely to cause cascading renders in practice, you can address the lint violation by deferring:Proposed fix
} catch (err) { - setError(err instanceof Error ? err.message : String(err)); - return () => {}; + const msg = err instanceof Error ? err.message : String(err); + queueMicrotask(() => { + if (!isCancelled) setError(msg); + }); + return () => { isCancelled = true; }; }
85-94: Avoidanyfor the Lottie params object.
params: anydrops all type safety. Consider using theAnimationConfigWithPath/AnimationConfigWithDataunion fromlottie-webtypes, or at least a more specific inline type.packages/motionforge/src/demo/DemoLottie.tsx (1)
1-130: Near-identical duplication withsrc/lib/remotion/demo/DemoLottie.tsx.These two files are effectively copy-pasted (only the import path differs). Consider extracting the shared demo into a single location and re-exporting, or generating one from the other, to avoid the two files drifting apart over time.
src/lib/remotion/components/Lottie.tsx (2)
121-123: ESLint: synchronoussetStatein effect catch block.The static analysis flags
setErrorcalled synchronously inside the effect body. In this case it's in acatchafter a synchronousloadAnimationcall, so it won't loop, but you could move the error into a ref and trigger a re-render via a separate mechanism, or simply suppress the rule here with an inline disable comment if you consider it acceptable.
160-164:styleobject inuseMemodeps may defeat memoization.Similar to the
srcissue, if the caller passes an inlinestyleobject, the reference changes every render, causingcontainerStyleto recompute. This is less critical than thesrcissue (no teardown/rebuild), but worth noting for consistency.
| <Lottie | ||
| src="https://assets.lottiefiles.com/packages/lf20_u4j3X6.json" | ||
| playbackRate={0.5} | ||
| loop | ||
| /> |
There was a problem hiding this comment.
External Lottie URL in a starter template is fragile and inconsistent with the demo.
This template hardcodes https://assets.lottiefiles.com/... while the DemoLottie files use https://assets10.lottiefiles.com/... (different subdomain). If either URL goes stale, users see a broken hello-world experience on first run.
Consider bundling a small inline JSON animation (like the simpleLottie object used in DemoLottie.tsx) instead of relying on an external URL for the starter template.
🤖 Prompt for AI Agents
In `@packages/create-motionforge/templates/hello-world/page.tsx.template` around
lines 49 - 53, The page.tsx.template currently uses an external Lottie URL in
the <Lottie> component which is fragile; replace that external src usage by
bundling an inline Lottie JSON (like the simpleLottie object defined in
DemoLottie.tsx) and pass it to the Lottie component (or import
DemoLottie/simpleLottie) so the starter template renders reliably offline and
matches the demo; update the <Lottie> usage in page.tsx.template to accept the
inlined JSON instead of the hardcoded "https://assets.lottiefiles.com/..." URL
and keep the existing playbackRate and loop props.
| "motionforge": "^1.2.0", | ||
| "lottie-web": "^5.13.0", |
There was a problem hiding this comment.
Template references motionforge ^1.2.0 but Lottie requires ^1.3.0.
The template now includes lottie-web and presumably uses the new <Lottie /> component, but the motionforge dependency is still pinned to ^1.2.0. Projects scaffolded from this template could resolve to 1.2.x (which lacks Lottie support) if 1.3.0 isn't yet published.
♻️ Suggested fix
- "motionforge": "^1.2.0",
+ "motionforge": "^1.3.0",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "motionforge": "^1.2.0", | |
| "lottie-web": "^5.13.0", | |
| "motionforge": "^1.3.0", | |
| "lottie-web": "^5.13.0", |
🤖 Prompt for AI Agents
In `@packages/create-motionforge/templates/shared/package.json.template` around
lines 12 - 13, Update the package version for the "motionforge" dependency in
package.json.template from "^1.2.0" to "^1.3.0" so it is compatible with the
included "lottie-web" dependency; locate the "motionforge" entry in the shared
package.json.template and bump the version string to "^1.3.0" (ensure the
trailing comma and JSON formatting remain valid).
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## [1.3.0] - 2024-03-20 |
There was a problem hiding this comment.
Incorrect changelog date.
The date 2024-03-20 appears to be incorrect — this PR was created on 2026-02-15. Update to the actual release date.
-## [1.3.0] - 2024-03-20
+## [1.3.0] - 2026-02-15📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## [1.3.0] - 2024-03-20 | |
| ## [1.3.0] - 2026-02-15 |
🤖 Prompt for AI Agents
In `@packages/motionforge/CHANGELOG.md` at line 8, Update the release date in the
changelog header for version marker "## [1.3.0] - 2024-03-20" to the correct
release date (replace 2024-03-20 with 2026-02-15) so the header reads "##
[1.3.0] - 2026-02-15"; locate the exact header line containing "## [1.3.0]" in
CHANGELOG.md and change only the date portion.
| useEffect(() => { | ||
| if (typeof window === 'undefined' || !containerRef.current) return; | ||
|
|
||
| let isCancelled = false; | ||
|
|
||
| const params: any = { | ||
| container: containerRef.current, | ||
| renderer: 'svg', | ||
| loop: false, | ||
| autoplay: false, | ||
| rendererSettings: { | ||
| progressiveLoad: false, | ||
| hideOnTransparent: true, | ||
| } | ||
| }; | ||
|
|
||
| if (typeof src === 'string') { | ||
| params.path = src; | ||
| } else { | ||
| params.animationData = src; | ||
| } | ||
|
|
||
| try { | ||
| const anim = lottie.loadAnimation(params); | ||
| animationRef.current = anim; | ||
|
|
||
| const onLoaded = () => { | ||
| if (!isCancelled) { | ||
| setIsLoaded(true); | ||
| } | ||
| }; | ||
|
|
||
| // Both events can be useful depending on how Lottie is loaded | ||
| anim.addEventListener('DOMLoaded', onLoaded); | ||
| anim.addEventListener('data_ready', onLoaded); | ||
|
|
||
| return () => { | ||
| isCancelled = true; | ||
| anim.destroy(); | ||
| animationRef.current = null; | ||
| }; | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : String(err)); | ||
| return () => {}; | ||
| } | ||
| }, [src]); |
There was a problem hiding this comment.
Object src in dependency array causes re-initialization on every render.
When src is an inline object (e.g., <Lottie src={{ ... }} />), a new reference is created each render, causing useEffect to destroy and re-create the lottie instance in an infinite loop. This is the primary usage pattern shown in the demos (src={simpleLottie} is fine since it's module-scoped, but any consumer passing an inline object will hit this).
Additionally, the error state is never cleared when src changes, so after a failed load, switching to a valid src still shows the error UI.
Proposed fix
+ const srcRef = useRef(src);
+ const [srcKey, setSrcKey] = useState(0);
+
+ // Only re-trigger when src *actually* changes (deep-compare for objects)
+ useEffect(() => {
+ if (typeof src === 'string') {
+ if (src !== srcRef.current) {
+ srcRef.current = src;
+ setSrcKey(k => k + 1);
+ }
+ } else {
+ // For objects, JSON-serialize or use a stable reference from the caller
+ srcRef.current = src;
+ }
+ }, [src]);
// Handle initialization
useEffect(() => {
if (typeof window === 'undefined' || !containerRef.current) return;
let isCancelled = false;
+ setError(null);
+ setIsLoaded(false);
// ... rest unchanged
- }, [src]);
+ }, [srcKey]);Alternatively, document that src must be a stable reference (module-level constant or useMemo'd) and add a note in the JSDoc. A simpler approach is to JSON.stringify the object src for the dep array:
+ const srcStable = useMemo(
+ () => (typeof src === 'string' ? src : JSON.stringify(src)),
+ [src]
+ );
// ...
- }, [src]);
+ }, [srcStable]);But note JSON.stringify itself re-runs on every render for object src, so the caller should ideally stabilize the reference. At minimum, clear error/loaded state on re-init.
🧰 Tools
🪛 ESLint
[error] 122-122: Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
- Update external systems with the latest state from React.
- Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/jailuser/git/packages/motionforge/src/components/Lottie.tsx:122:7
120 | };
121 | } catch (err) {
122 | setError(err instanceof Error ? err.message : String(err));
| ^^^^^^^^ Avoid calling setState() directly within an effect
123 | return () => {};
124 | }
125 | }, [src]);
(react-hooks/set-state-in-effect)
🤖 Prompt for AI Agents
In `@packages/motionforge/src/components/Lottie.tsx` around lines 80 - 125, The
effect currently re-initializes whenever the inline object `src` reference
changes, causing infinite re-creates and never clearing prior errors; update the
effect in the component that uses useEffect (the effect that references `src`,
`containerRef`, `animationRef`, `setIsLoaded`, and `setError`) to compute a
stable key for `src` (e.g., use string key = typeof src === 'string' ? src :
JSON.stringify(src)), track the previous key with a ref and bail out early if
the key hasn't changed, and when the key does change reset state by calling
setError(null) and setIsLoaded(false) before creating the new lottie instance;
then use that stable key (not the raw object) in the effect dependency array so
inline object props no longer force re-initialization.
| // Map MotionForge currentFrame to Lottie frame | ||
| // playbackRate affects how fast we move through the Lottie timeline | ||
| let targetFrame = currentFrame * playbackRate; | ||
|
|
||
| if (loop) { | ||
| targetFrame = targetFrame % duration; | ||
| } else { | ||
| targetFrame = Math.min(targetFrame, duration - 0.01); | ||
| } | ||
|
|
||
| // finalFrame is relative to the Lottie's original timeline | ||
| const finalFrame = start + targetFrame; | ||
|
|
||
| // goToAndStop(value, isFrame) - second arg true means it's a frame number, not time | ||
| anim.goToAndStop(finalFrame, true); |
There was a problem hiding this comment.
Frame mapping doesn't clamp negative values, breaking with negative playbackRate or edge cases.
- Line 145:
currentFrame * playbackRatecan be negative ifplaybackRate < 0(not validated). - Line 148: JS
%operator preserves the sign of the dividend, so a negativetargetFrame % durationyields a negative result, causingfinalFrameto fall belowstart. - Line 150: Non-loop path uses
Math.minbut noMath.max(0, ...), so negative values pass through unclamped.
Proposed fix
let targetFrame = currentFrame * playbackRate;
if (loop) {
- targetFrame = targetFrame % duration;
+ targetFrame = ((targetFrame % duration) + duration) % duration;
} else {
- targetFrame = Math.min(targetFrame, duration - 0.01);
+ targetFrame = Math.max(0, Math.min(targetFrame, duration - 0.01));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Map MotionForge currentFrame to Lottie frame | |
| // playbackRate affects how fast we move through the Lottie timeline | |
| let targetFrame = currentFrame * playbackRate; | |
| if (loop) { | |
| targetFrame = targetFrame % duration; | |
| } else { | |
| targetFrame = Math.min(targetFrame, duration - 0.01); | |
| } | |
| // finalFrame is relative to the Lottie's original timeline | |
| const finalFrame = start + targetFrame; | |
| // goToAndStop(value, isFrame) - second arg true means it's a frame number, not time | |
| anim.goToAndStop(finalFrame, true); | |
| // Map MotionForge currentFrame to Lottie frame | |
| // playbackRate affects how fast we move through the Lottie timeline | |
| let targetFrame = currentFrame * playbackRate; | |
| if (loop) { | |
| targetFrame = ((targetFrame % duration) + duration) % duration; | |
| } else { | |
| targetFrame = Math.max(0, Math.min(targetFrame, duration - 0.01)); | |
| } | |
| // finalFrame is relative to the Lottie's original timeline | |
| const finalFrame = start + targetFrame; | |
| // goToAndStop(value, isFrame) - second arg true means it's a frame number, not time | |
| anim.goToAndStop(finalFrame, true); |
🤖 Prompt for AI Agents
In `@packages/motionforge/src/components/Lottie.tsx` around lines 143 - 157, The
mapping of currentFrame to Lottie frames can produce negative targetFrame when
playbackRate is negative and the modulo preserves sign; update the targetFrame
calculation in the Lottie component (symbols: currentFrame, playbackRate, loop,
duration, start, targetFrame, finalFrame, anim.goToAndStop) so that after
computing targetFrame = currentFrame * playbackRate you: for loop=true normalize
the modulo to a non-negative value (e.g. ((targetFrame % duration) + duration) %
duration) and for loop=false clamp to the [0, duration - 0.01] range (use
Math.max(0, ...) or equivalent), then compute finalFrame = start + targetFrame
and call anim.goToAndStop(finalFrame, true).
| useEffect(() => { | ||
| if (typeof window === 'undefined' || !containerRef.current) return; | ||
|
|
||
| let isCancelled = false; | ||
|
|
||
| const params: any = { | ||
| container: containerRef.current, | ||
| renderer: 'svg', | ||
| loop: false, | ||
| autoplay: false, | ||
| rendererSettings: { | ||
| progressiveLoad: false, | ||
| hideOnTransparent: true, | ||
| } | ||
| }; | ||
|
|
||
| if (typeof src === 'string') { | ||
| params.path = src; | ||
| } else { | ||
| params.animationData = src; | ||
| } | ||
|
|
||
| try { | ||
| const anim = lottie.loadAnimation(params); | ||
| animationRef.current = anim; | ||
|
|
||
| const onLoaded = () => { | ||
| if (!isCancelled) { | ||
| setIsLoaded(true); | ||
| } | ||
| }; | ||
|
|
||
| // Both events can be useful depending on how Lottie is loaded | ||
| anim.addEventListener('DOMLoaded', onLoaded); | ||
| anim.addEventListener('data_ready', onLoaded); | ||
|
|
||
| return () => { | ||
| isCancelled = true; | ||
| anim.destroy(); | ||
| animationRef.current = null; | ||
| }; | ||
| } catch (err) { | ||
| setError(err instanceof Error ? err.message : String(err)); | ||
| return () => {}; | ||
| } | ||
| }, [src]); |
There was a problem hiding this comment.
isLoaded is not reset when src changes, causing stale reads on the new animation.
When src changes, the cleanup (line 117-119) destroys the old animation and nulls the ref, but isLoaded remains true. The new effect body immediately assigns the fresh (not-yet-loaded) AnimationItem to animationRef.current (line 104). Because isLoaded is still true, the frame-sync effect (line 128) will fire and call anim.totalFrames / anim.goToAndStop on an animation whose data hasn't loaded yet, which can produce incorrect rendering or throw.
Proposed fix — reset loading state at the top of the init effect
useEffect(() => {
if (typeof window === 'undefined' || !containerRef.current) return;
+ setIsLoaded(false);
+ setError(null);
+
let isCancelled = false;🧰 Tools
🪛 ESLint
[error] 122-122: Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
- Update external systems with the latest state from React.
- Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/jailuser/git/src/lib/remotion/components/Lottie.tsx:122:7
120 | };
121 | } catch (err) {
122 | setError(err instanceof Error ? err.message : String(err));
| ^^^^^^^^ Avoid calling setState() directly within an effect
123 | return () => {};
124 | }
125 | }, [src]);
(react-hooks/set-state-in-effect)
🤖 Prompt for AI Agents
In `@src/lib/remotion/components/Lottie.tsx` around lines 80 - 125, When src
changes, reset the loading state at the start of the init effect to avoid stale
true values: inside the useEffect that depends on src (the effect creating the
lottie AnimationItem and using animationRef), call setIsLoaded(false) before
initiating load, and also clear any previous setError if present; ensure the
onLoaded callback still sets setIsLoaded(true) when DOMLoaded/data_ready fire
and that cleanup still destroys anim and nulls animationRef.current to prevent
races.
| setError(err instanceof Error ? err.message : String(err)); | ||
| return () => {}; | ||
| } | ||
| }, [src]); |
There was a problem hiding this comment.
Object src as a dependency causes infinite re-initialization.
When src is an inline object literal (e.g. <Lottie src={{...}} />), a new reference is created every render, so this effect re-runs on every frame — destroying and recreating the animation each time. Consider either documenting that callers must memoize the object, or stabilizing the dependency internally (e.g. via JSON.stringify).
Option: stabilize with a serialized key
+ const srcKey = useMemo(
+ () => (typeof src === 'string' ? src : JSON.stringify(src)),
+ [src],
+ );
+
useEffect(() => {
// ...init...
- }, [src]);
+ }, [srcKey]);Note: useMemo with [src] alone still compares by reference, so JSON.stringify would need to be moved outside or use a useRef-based deep-compare. A simpler approach is to document/enforce that callers pass a stable reference.
🤖 Prompt for AI Agents
In `@src/lib/remotion/components/Lottie.tsx` at line 125, The effect in the Lottie
component currently lists src in its dependency array which causes infinite
re-initialization when callers pass an inline object; update the hook to
stabilize the dependency by deriving a serialized key (e.g. const srcKey =
useMemo(() => typeof src === 'string' ? src : JSON.stringify(src), [src]) or
compute a ref-based deep-compare) and use srcKey in the effect dependency
instead of src (or alternatively document that callers must memoize src); locate
the effect in the Lottie component that currently ends with "}, [src]);" and
replace the dependency with the derived srcKey so the animation is not
destroyed/recreated on every render.
| // Map MotionForge currentFrame to Lottie frame | ||
| // playbackRate affects how fast we move through the Lottie timeline | ||
| let targetFrame = currentFrame * playbackRate; | ||
|
|
||
| if (loop) { | ||
| targetFrame = targetFrame % duration; | ||
| } else { | ||
| targetFrame = Math.min(targetFrame, duration - 0.01); | ||
| } | ||
|
|
||
| // finalFrame is relative to the Lottie's original timeline | ||
| const finalFrame = start + targetFrame; |
There was a problem hiding this comment.
Frame mapping can produce negative Lottie frames when currentFrame is 0 and playbackRate is applied.
The mapping targetFrame = currentFrame * playbackRate is correct for non-negative currentFrame, but if relativeFrame happens to be negative (e.g., due to timing edge cases in Sequence), targetFrame will be negative and finalFrame will be before start. Consider clamping:
- let targetFrame = currentFrame * playbackRate;
+ let targetFrame = Math.max(0, currentFrame * playbackRate);This is a minor edge case since Sequence doesn't render children when inactive, but it adds defensive safety.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Map MotionForge currentFrame to Lottie frame | |
| // playbackRate affects how fast we move through the Lottie timeline | |
| let targetFrame = currentFrame * playbackRate; | |
| if (loop) { | |
| targetFrame = targetFrame % duration; | |
| } else { | |
| targetFrame = Math.min(targetFrame, duration - 0.01); | |
| } | |
| // finalFrame is relative to the Lottie's original timeline | |
| const finalFrame = start + targetFrame; | |
| // Map MotionForge currentFrame to Lottie frame | |
| // playbackRate affects how fast we move through the Lottie timeline | |
| let targetFrame = Math.max(0, currentFrame * playbackRate); | |
| if (loop) { | |
| targetFrame = targetFrame % duration; | |
| } else { | |
| targetFrame = Math.min(targetFrame, duration - 0.01); | |
| } | |
| // finalFrame is relative to the Lottie's original timeline | |
| const finalFrame = start + targetFrame; |
🤖 Prompt for AI Agents
In `@src/lib/remotion/components/Lottie.tsx` around lines 143 - 154, The
targetFrame calculation can become negative when relativeFrame/currentFrame is
negative; update the mapping in the Lottie component so targetFrame is clamped
to a non-negative value before applying looping or duration bounds.
Specifically, in the block computing targetFrame (symbols: currentFrame,
playbackRate, targetFrame, loop, duration) ensure you compute targetFrame =
Math.max(0, currentFrame * playbackRate) (or equivalent) and then apply the loop
modulo or Math.min clipping, and ensure finalFrame (symbols: start, finalFrame)
cannot end up before start by preserving that clamp. This keeps defensive safety
for edge cases where relativeFrame may be negative.
| const RelativeFrameContext = createContext<number | null>(null); | ||
|
|
||
| export const useRelativeCurrentFrame = () => useContext(RelativeFrameContext); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all usages of useRelativeCurrentFrame
rg 'useRelativeCurrentFrame' -t ts -t tsx --no-heading -nRepository: codedbytahir/motionforge
Length of output: 95
🏁 Script executed:
#!/bin/bash
# Find all usages of useRelativeCurrentFrame in .ts and .tsx files
rg 'useRelativeCurrentFrame' --glob '*.ts' --glob '*.tsx' -nRepository: codedbytahir/motionforge
Length of output: 4363
🏁 Script executed:
#!/bin/bash
# Read Lottie.tsx around the usage site
sed -n '70,80p' src/lib/remotion/components/Lottie.tsxRepository: codedbytahir/motionforge
Length of output: 475
Type definition mismatch: exported type declaration is outdated.
useRelativeCurrentFrame now returns number | null, but the exported type declaration in packages/motionforge/dist/index.d.ts:214 still declares the return type as number. This inconsistency will mislead external consumers. The Lottie component correctly handles the nullable return value with a null check, but the dist types need to be updated to reflect the actual implementation.
🤖 Prompt for AI Agents
In `@src/lib/remotion/components/Sequence.tsx` around lines 110 - 112, The
exported type for useRelativeCurrentFrame is incorrect in the distributed types;
update the declaration so useRelativeCurrentFrame returns number | null to match
the implementation that uses RelativeFrameContext (createContext<number | null>)
and the hook useRelativeCurrentFrame; find the exported type/signature for
useRelativeCurrentFrame in the generated declaration file (the dist/index.d.ts
export for useRelativeCurrentFrame) and change its return type from number to
number | null so external consumers see the nullable return.
Implemented a production-grade Lottie animation system for MotionForge. The system ensures frame-perfect rendering by synchronizing lottie-web with the internal MotionForge frame clock, disabling standard time-based autoplay. It supports advanced features like playback segments (slicing), variable playback rates, and automatic sequence mapping. The CLI and documentation were also updated to provide a seamless developer experience.
PR created automatically by Jules for task 209608830295865078 started by @codedbytahir
Summary by CodeRabbit
Release Notes
New Features
Documentation
Version