Skip to content

MobX React plugin - #4735

Draft
kubk wants to merge 23 commits into
mainfrom
mobx-react-plugin
Draft

kubk wants to merge 23 commits into
mainfrom
mobx-react-plugin

Conversation

@kubk

@kubk kubk commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3874
Fixes #4594
Fixes #3836
Fixes #4734


A plugin that greatly simplifies using MobX with React, while still allowing React Compiler in the same project. So this code:

export function Dice() {
  return (
    <button disabled={dice.isRolling} onClick={() => dice.roll()}>
      {dice.isRolling ? "Rolling..." : `You rolled ${dice.value}`}
    </button>
  )
}

class DiceStore {
  value = 1
  isRolling = false

  constructor() {
    makeAutoObservable(this)
  }

  async roll() {
    this.isRolling = true
    await sleep(1000)
    this.value = Math.ceil(Math.random() * 6)
    this.isRolling = false
  }
}

will be transformed into this:

import { observer } from "mobx-react-lite"

export const Dice = observer(function Dice() {
  "use no memo"

  return (
    <button disabled={dice.isRolling} onClick={() => dice.roll()}>
      {dice.isRolling ? "Rolling..." : `You rolled ${dice.value}`}
    </button>
  )
})

class DiceStore {
  value = 1
  isRolling = false

  constructor() {
    makeAutoObservable(this)
  }

  *roll() {
    this.isRolling = true
    yield sleep(1000)
    this.value = Math.ceil(Math.random() * 6)
    this.isRolling = false
  }
}

It does 3 things:

  • Wraps all components in observer, so a forgotten observer can't cause bugs. Files/folders can be excluded
  • Lets you enable React Compiler. Components get "use no memo", so MobX owns their re-rendering. Everything else can be memoized by the compiler. For example a library like shadcn can be compiled and excluded from observation (which is what I do in many of my projects often)
  • Fixes async/await with MobX. MobX has multiple ways to handle async code, none of which are ideal. async/await needs runInAction(), promises need action(), and both add nesting. yield + flow avoids the nesting but has TS issues. The plugin lets you write plain async methods and compiles them to flows

I'm very optimistic about 1 and 3. Regarding 2 - it's a compromise. Other mutable state managers like legend-state suffer from the same issue - their observer isn't compatible with React Compiler. Their solution is to use a selector hook to explicitly access state. I'm still on the fence here. Going the same road in MobX would mean giving up the automatic dependency discovery. e.g:

function Dashboard() {
  return (
    <div>
      <h1>{user.name}</h1>
      {user.isAdmin && <Stats data={store.adminStats} />}
    </div>
  )
}

Yet a useValue hook would solve one issue with the current plugin approach: the plugin has to exclude all hooks from React Compiler memoization too, not only components, because any hook that reads observables can go stale. So it all boils down to

  • continue using MobX the way it's intended to use (via observer) and have a plugin that turns off Compiler where MobX is used
  • become fully compatible with the Compiler but sacrifice the automatic dependency discovery + all the MobX apps have to be rewritten. MobX becomes fully compatible without any plugins but it'd still require some lint rule to disallow reading MobX without useValue in components

Based on my shallow perf tests MobX + plugin is little bit faster than MobX + useValue and compiler on

Any feedback is appreciated here. Maybe a Twitter poll would help? @mweststrate

Status

Still WIP. I'm trimming the tests, because many of them too fragile and look like white box testing

@changeset-bot

changeset-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 875b03c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
mobx-react-plugin Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@kubk
kubk requested a balanced review from Copilot October 13, 2026 14:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unsafe flow detection, missed observer aliases, discarded Vite transforms, and an invalid Next.js configuration must be corrected.

6 open findings
What changed in this PR

Introduces mobx-react-plugin, a Babel/Vite transform integrating MobX with React Compiler and converting eligible async methods into flows.

Changes:

  • Adds automatic observer, "use no memo", and flow transforms.
  • Adds extensive unit, runtime, React Native, and Vite coverage.
  • Adds documentation, examples, workspace integration, and shared Jest setup.
File Description
website/​sidebars.json Adds React Compiler documentation.
packages/​mobx/​jest.setup.ts Uses shared MobX test setup.
packages/​mobx-react/​jest.setup.ts Uses shared React test setup.
packages/​mobx-react-lite/​jest.setup.ts Uses shared React test setup.
jest.setup.base.ts Centralizes MobX test-state cleanup.
package.json Registers the new workspace.
packages/​mobx-react-plugin/​package.json Defines package metadata and dependencies.
packages/​mobx-react-plugin/​LICENSE Adds the MIT license.
packages/​mobx-react-plugin/​tsconfig.json Configures production compilation.
packages/​mobx-react-plugin/​tsconfig.test.json Configures test type-checking.
packages/​mobx-react-plugin/​jest.config.js Configures plugin tests.
packages/​mobx-react-plugin/​jest.setup.ts Initializes MobX test state.
packages/​mobx-react-plugin/​src/​babel.ts Provides the Babel entry point.
packages/​mobx-react-plugin/​src/​vite.ts Provides the Vite integration.
packages/​mobx-react-plugin/​src/​options.ts Defines and normalizes options.
packages/​mobx-react-plugin/​src/​transform.ts Implements component and directive transforms.
packages/​mobx-react-plugin/​src/​flow.ts Implements async-to-flow conversion.
packages/​mobx-react-plugin/​README.md Documents installation, behavior, and options.
packages/​mobx-react-plugin/​__tests__/​vite.test.ts Tests Vite integration.
packages/​mobx-react-plugin/​__tests__/​transform.test.ts Tests component transforms.
packages/​mobx-react-plugin/​__tests__/​runtime.test.tsx Tests React runtime behavior.
packages/​mobx-react-plugin/​__tests__/​react-native.test.ts Tests the Metro pipeline.
packages/​mobx-react-plugin/​__tests__/​react-compiler.test.ts Tests React Compiler interoperability.
packages/​mobx-react-plugin/​__tests__/​flow.test.ts Tests flow transformations.
packages/​mobx-react-plugin/​__tests__/​flow-runtime.test.ts Tests transformed flows at runtime.
packages/​mobx-react-plugin/​__tests__/​utils/​transform.ts Adds transformation test helpers.
packages/​mobx-react-plugin/​__tests__/​utils/​compiled.ts Detects compiler-generated memoization.
packages/​mobx-react-plugin/​__tests__/​fixtures/​rn-project/​babel.config.js Configures the React Native fixture.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​.gitignore Ignores generated example files.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​package.json Defines the example application.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​tsconfig.json Configures example TypeScript.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​vite.config.ts Configures the demonstration pipeline.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​index.html Adds the example page shell.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​README.md Documents the example.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​src/​App.tsx Demonstrates protected and unprotected components.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​src/​main.tsx Mounts the example application.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​src/​store.ts Provides observable example state.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​src/​controls/​UnsafeManualObserver.tsx Demonstrates stale compiled output.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​src/​protected/​AutoObservedCounter.tsx Demonstrates automatic observation.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​src/​protected/​AutoObservedTodos.tsx Demonstrates observable collection updates.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​src/​pure/​PureReactComponent.tsx Demonstrates normal compiler optimization.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​tests/​transform.spec.ts Verifies final Vite output.
packages/​mobx-react-plugin/​examples/​react-compiler-vite/​tests/​runtime.spec.tsx Verifies example runtime behavior.
docs/​react-compiler.md Adds React Compiler guidance.
docs/​react-integration.md Links automatic integration guidance.
docs/​installation.md Lists the new React plugin.
docs/​actions.md Documents automatic flow conversion.
.changeset/​mobx-react-plugin-package.md Records the new package release.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/mobx-react-plugin/src/flow.ts
Comment thread packages/mobx-react-plugin/src/flow.ts
Comment thread packages/mobx-react-plugin/src/transform.ts
Comment thread packages/mobx-react-plugin/src/transform.ts
Comment thread packages/mobx-react-plugin/src/vite.ts Outdated
Comment thread packages/mobx-react-plugin/README.md Outdated
@mweststrate

mweststrate commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

This looks pretty awesome @kubk !

I'm still on the fence here. Going the same road in MobX would mean giving up the automatic dependency discovery.

Imho this is the right trade-off. Automatic dependency discovery is one of the strong suits of MobX (although we could still offer the alternative as well, if we wanted to, by introducing an useMobX<T>(() => T) (or useObserve / useValue) hook. But that can just be a separate PR on mobx-react-lite)

Separate Q: Should we also offer a "no mobx" directive, to make sure people can easily opt out of the transformation?

@kubk

kubk commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator Author

@mweststrate

Separate Q: Should we also offer a "no mobx" directive, to make sure people can easily opt out of the transformation?

Right now we use the existing "use memo" directive for that.

So the plugin wraps components in observer and adds "use no memo". When it sees "use memo", it skips that component / hook so the Compiler can take over. We also throw on observer + "use memo", since they're mutually exclusive

I'll make sure this opt-out is documented. There's no per store opt-out for the async -> flow conversion (not sure if it's needed?). There's a global switch in the build tool config + plus file-level exclude:

mobxReactPlugin({ flow: false })

mobxReactPlugin({
  exclude: ["src/stores/legacy/**", /LegacyStore\.ts$/]
})

This branch has not been deployed

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

Labels

None yet

Projects

None yet

3 participants