Skip to content

Fix LazyMotion not working with motion/react-m subpath (#3091)#3710

Open
mattgperry wants to merge 1 commit intomainfrom
fix/issue-3091
Open

Fix LazyMotion not working with motion/react-m subpath (#3091)#3710
mattgperry wants to merge 1 commit intomainfrom
fix/issue-3091

Conversation

@mattgperry
Copy link
Copy Markdown
Collaborator

Summary

Fixes #3091<LazyMotion> from motion/react couldn't load features for <m.div> imported from motion/react-m. When users wrapped m components from the /m subpath in a <LazyMotion> provider, the m components silently rendered without animations, gestures, or layout features.

Cause

The CJS bundles for the main framer-motion entry and the framer-motion/m subpath were emitted in separate rollup invocations, with a comment that intentionally avoided sharing chunks. Each CJS bundle therefore made its own createContext() calls for LazyContext, MotionContext, MotionConfigContext, etc.

createContext() returns a unique object each time it is called, so the two bundles ended up with two distinct React Context instances. The <LazyMotion> from motion/react published the renderer and feature definitions on its bundle's LazyContext; the <m.div> from motion/react-m read from a different LazyContext whose value was the default { strict: false } (no renderer). The m component never built a VisualElement, so nothing animated.

ESM consumers were unaffected because the ESM build uses preserveModules: true, so the shared dist/es/context/LazyContext.mjs resolves to a single module.

Fix

Bundle lib/m.js together with lib/index.js in the same CJS rollup invocation. Rollup hoists shared modules (the contexts, createMotionComponent, useVisualElement, etc.) into a common chunk that both entry points require, so framer-motion/dist/cjs/index.js and framer-motion/dist/cjs/m.js now reference the same context instances. ESM and the existing per-entry CJS bundles for dom, dom-mini, mini, and debug are unchanged.

Regression coverage

  • packages/framer-motion/scripts/check-bundle.js now fails the build if dist/cjs/m.js re-introduces its own createContext({ strict: false }) call.
  • dev/react/src/tests/lazy-motion-react-m-subpath.tsx + packages/framer-motion/cypress/integration/lazy-motion-react-m-subpath.ts exercise <LazyMotion features={domAnimation}> wrapping <m.div> from motion/react-m.

Test plan

  • yarn build succeeds (check-bundle gate passes).
  • yarn test — 793 unit tests pass.
  • Cypress lazy-motion-react-m-subpath passes on React 18.
  • Cypress lazy-motion-react-m-subpath passes on React 19.

🤖 Generated with Claude Code

…ndles

The CJS bundles for the main `framer-motion` entry and the `/m` subpath
were rolled up in separate invocations, so each emitted its own copy of
`createContext()` for `LazyContext`, `MotionContext`, etc. When users
combined `<LazyMotion>` from `motion/react` with `<m.div>` from
`motion/react-m`, the LazyMotion provider and the m component
subscribed to two different React Context instances, so the renderer
and feature definitions never reached the m component.

Bundling `lib/m.js` alongside `lib/index.js` in a single rollup CJS
invocation lets rollup share the contexts via a common chunk, so both
entry points reference the same context instances. ESM was already
fine via `preserveModules`, so only the CJS structure changes.

Adds a regression check in `check-bundle.js` and a Cypress smoke test
for `<LazyMotion>` + `<m.div>` across the `motion/react` and
`motion/react-m` subpaths.

Fixes #3091
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 9, 2026

Greptile Summary

This PR fixes a long-standing silent failure (#3091) where <LazyMotion> from motion/react could not supply its renderer and feature definitions to <m.div> imported from motion/react-m in CJS environments. The root cause was that each CJS entry point was bundled in its own Rollup invocation, causing separate createContext() calls and thus distinct React context instances between the two packages.

  • Build fix: lib/m.js is added to the cjs Rollup invocation alongside lib/index.js and lib/client.js, causing Rollup to hoist shared modules (all React contexts, createMotionComponent, useVisualElement, etc.) into a single common chunk that both entries require.
  • Regression guard: check-bundle.js now asserts that dist/cjs/m.js does not contain its own createContext({ strict: false }) call, failing the build if the context-sharing contract is broken.
  • Integration test: A new Cypress test wraps <m.div> from motion/react-m inside <LazyMotion features={domAnimation}> from motion/react and verifies the animation completes.

Confidence Score: 5/5

Safe to merge — the change is a narrow Rollup config adjustment that co-bundles lib/m.js with lib/index.js, and includes a build-time guard and integration tests to validate the fix.

The build change is minimal and well-contained: one line added to the cjs input array and one standalone cjsM config removed. The shared-chunk mechanism is standard Rollup behavior and doesn't alter the public API or runtime behavior of either entry point. The regression guard in check-bundle.js ensures the fix can't silently regress. The only notable issue is that the Cypress test's animationFailed assertion is vacuously true at the 500ms check point, but the animationComplete assertion provides genuine regression coverage for the bug.

No files require special attention; the one nit is in the Cypress test timing logic.

Important Files Changed

Filename Overview
packages/framer-motion/rollup.config.mjs Core fix: lib/m.js added to the multi-entry cjs invocation; standalone cjsM config removed. Other separate CJS bundles (dom, mini, debug, dom-mini) remain intentionally isolated.
packages/framer-motion/scripts/check-bundle.js Adds a targeted regression guard that fails the build if dist/cjs/m.js re-introduces its own createContext({ strict: false }) call.
packages/framer-motion/cypress/integration/lazy-motion-react-m-subpath.ts New Cypress integration test; the animationFailed assertion is vacuously true at the 500ms check point (the timeout fires at 1000ms), but the animationComplete assertion provides meaningful coverage.
dev/react/src/tests/lazy-motion-react-m-subpath.tsx New test fixture component for the regression; uses onAnimationComplete callback and a 1s timeout to set data attributes that Cypress reads.
dev/react/package.json Adds motion as a dev dependency so the new test fixture can import from motion/react and motion/react-m.
CHANGELOG.md Changelog entry added for the LazyMotion CJS context-sharing fix under the current unreleased version.

Reviews (1): Last reviewed commit: "Share React contexts between framer-moti..." | Re-trigger Greptile

Comment on lines +7 to +9
expect($element.dataset.animationFailed).to.not.equal("true")
expect($element.dataset.animationComplete).to.equal("true")
expect(getComputedStyle($element).opacity).to.equal("1")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 animationFailed assertion is vacuously true at the assertion point

The data-animation-failed attribute is set by the React component's setTimeout at 1000 ms, but Cypress checks at .wait(500) — before the timeout can ever fire. The assertion expect($element.dataset.animationFailed).to.not.equal("true") will therefore always pass, even if the animation silently fails to start. The real regression protection comes entirely from the animationComplete check on the next line. If you want the failure-path assertion to be meaningful, either increase .wait() beyond 1000 ms, or drop the animationFailed check since it adds no coverage here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] LazyMotion is not working in motion@^12.4.3

1 participant