mobxjs/mobx
github.com/mobxjs/mobx ↗ · 349 files · 3.3 MB
MobX Simple, scalable state management. --- Documentation Documentation can be found at mobx.js.org.
Overview
MobX is a TypeScript state-management library for JavaScript applications, with React bindings, ESLint rules, and a legacy-decorator migration CLI in an npm workspace monorepo. Its core converts ordinary data into observable structures, records dependencies during reads, and propagates mutations through computed values and reactions. At commit 60be47cab0926d12f22449ec6c809fb5ec6601c4, distinctive implementation details include lazy decorator-backed values, batched reaction execution, and React observer cleanup combining native finalization with timers.
System map
The repository distributes libraries and developer tools through five npm workspaces. The core runtime owns an in-memory dependency graph, while React integration and source-analysis tools have separate package boundaries. Shared Rollup configuration produces consumer-specific bundles, and a separate Docusaurus package builds documentation.
- The private mobx-root manifest defines five workspaces and delegates build and build:test through npm workspace scripts.
- The core entrypoint is src/mobx.ts; its documented module boundaries separate public APIs, reactive algorithms, observable types, and utilities.
- MobX exports different artifacts for Node, native React environments, and ES imports, with TypeScript declarations and explicit source/dist subpath exports.
- MobXGlobals owns tracking contexts, batch depth, pending reactions, and pending unobservations; compatible instances share it through globalThis.__mobxGlobals.
- ObservableObjectAdministration stores per-property ObservableValue or ComputedValue instances in a Map and attaches to application objects through the hidden $mobx property.
- mobx-react depends on mobx-react-lite, re-exports its hooks and Observer component, and adds observer dispatch for class components; both packages declare React 18/19 peers.
- createRollupConfig always constructs development and production CJS configurations; TARGET=test skips other formats, while publish adds environment-specific ESM alongside general ESM, .mjs, and UMD outputs.
- Documentation uses Docusaurus build/start hooks and GitHub Pages publishing; package releases use Changesets in a workflow restricted to mobxjs/mobx with write permissions.
Feature catalog
The developer-facing surface centers on observable data, derived values, and managed side effects. React adapters connect the same graph to function and class rendering. Companion packages provide lint diagnostics and migration transforms, while the website exposes reference documentation and generated error explanations.
- observable dispatches plain objects, arrays, Maps, and Sets to specialized factories, preserves already-observable values and other object instances, and boxes remaining inputs.
- makeObservable accepts explicit annotations; makeAutoObservable infers annotations, supports overrides, optimizes plain objects through extendObservable, and rejects superclass-based or already-observable targets in development.
- Observable modifiers support deep conversion, shallow containers, reference preservation, and structural equality; Stage 3 observable accessors and computed getters register lazy factories.
- autorun tracks its callback, reaction separates a tracked expression from an action-wrapped effect with equality and fireImmediately options, and when offers a disposable callback or cancellable promise with timeout/abort handling.
- action batches synchronous changes, while flow drives generators through action-wrapped continuations and returns a promise whose cancel method invokes generator cleanup and rejects with FlowCancellationError.
- React APIs include observer, Observer, useLocalObservable, and static-rendering toggles; lite handles memo and forwardRef, while mobx-react adds class observers and rejects unsupported custom shouldComponentUpdate behavior.
- The core exports collection utilities, observe/intercept, observation lifecycle hooks, toJS, dependency/observer trees, spy, and reaction-error registration; its development entrypoint can inject diagnostics into a devtools hook.
- Developer tooling includes five ESLint rules with legacy/flat recommended configurations, a jscodeshift CLI accepting directory/path flags and transform options, and website reference navigation plus generated numeric error documentation.
Data flow
Inputs enter through observable factories, annotation maps, setters, and user-supplied callbacks. Enhancers transform values, administrations retain them, and synchronous reads build dependency edges. Mutations invalidate those edges and schedule effects or React updates; conversion and transport examples define the boundary toward external storage.
- observable factory options select an enhancer; deepEnhancer recursively wraps supported containers and converts previously unwrapped functions into autoAction or flow.
- Object creation captures property descriptors before administration attachment, then initObservable batches annotation application; observable properties receive shared accessors backed by administration entries.
- ObservableValue.get reports observation; reportObserved records each observable once per derivation run, and bindDependencies removes obsolete edges and adds newly read dependencies.
- Boxed writes pass mutation diagnostics, optional interceptors, enhancement, and equality suppression before storing the value; accepted changes report invalidation and notify listeners.
- Arrays retain enhanced items behind a Proxy and report reads through one atom; Maps distinguish key existence from value observation and notify the keys atom when inserting new entries.
- ComputedValue.get branches between unobserved computation and tracked caching; trackAndCompute confirms propagation only for suspension, exceptions, or comparer-detected changes.
- React observation converts Reaction invalidation into a new Symbol snapshot and an onStoreChange callback; useSyncExternalStore consumes stable subscription functions, and unsubscribe disposes the Reaction.
- toJS caches converted observable containers to preserve cycles and leaves non-observable branches unchanged; the documented persistence example instead derives asJson and hands it to an application-provided transportLayer.saveTodo.
Behavior traces
The source supports complete walks through import initialization, observable construction, mutation propagation, and React rendering. Additional traces follow conversion, application-level autosave, and codemod execution. The autosave trace is explicitly a documentation example; it does not establish a bundled persistence service.
- Import startup initializes or merges MobXGlobals during module evaluation, checks Symbol/Map/Set/Proxy availability in development, exports the public API, and conditionally calls injectMobx on the development tools hook.
- Explicit model initialization follows makeObservable → initObservable → asObservableObject → make_ → annotation.make_; observable annotation dispatch reaches defineObservableProperty_, which installs descriptors and constructs ObservableValue.
- Initial autorun follows autorun → new Reaction → schedule_ → runReactions → runReaction_ → track(reactionRunner) → trackDerivedFunction → view; an already-aborted signal prevents the initial schedule.
- A boxed action update follows createAction → executeAction → _startAction → ObservableValue.set → setNewValue_ → Atom.reportChanged → propagateChanged → Reaction.schedule_; _endAction → endBatch releases queued reactions after the enclosing batch finishes.
- Function rendering follows observer → optional forwardRef unwrapping → memo wrapper → useObserver → createObserverAdministration/createReaction → useSyncExternalStore → reaction.track(render); invalidation changes the snapshot, while unsubscription disposes the reaction.
- Abandoned-render cleanup follows registry.register → microtask flushRegistrations → native and timer registration → finalizeRegistration → observerFinalizationRegistry callback → Reaction.dispose; a later subscribe recreates a missing reaction and changes its snapshot to restore tracking.
- The documented autosave path is Todo constructor → makeAutoObservable → reaction(() => asJson) → action-wrapped effect → transportLayer.saveTodo when autoSave is true; Todo.dispose calls the stored reaction disposer.
- Migration execution follows interpret_cli_args → spawnBin/getCommandPath → jscodeshift with undecorate.ts → transform/parser → member collection/createConstructor → import insertion → source.toSource; unchanged files can return no transformed source.
Testing signals
Tests and CI were inspected, not executed; no passing result or coverage percentage is asserted. The bounded inspection found Jest/ts-jest suites, React Testing Library integration tests, ESLint RuleTester fixtures, and codemod subprocess tests. Assertions cover important semantics and resource lifecycles, but configured CI steps have limits that require distinguishing script names from effective checks.
- The inspected test configuration uses Jest projects, ts-jest transformation, jsdom, and a development global; coverage collection targets package TypeScript source, without supplying an execution result.
- Core fixtures assert that two writes inside an action produce only the final autorun value, and that AbortSignal disposal prevents subsequent reaction effects, including initial effects for pre-aborted signals.
- Flow tests assert FlowCancellationError rejection and execution of generator finally blocks; toJS tests assert identity preservation across cyclic objects, arrays, and Maps.
- React Suspense fixtures use Testing Library, fake timers, and observer-tree assertions to test abandoned renders, repeated retries, recovery before subscription, and preservation of committed observers during suspended transitions.
- Retention coverage uses forced GC and WeakRef to check collection of the first rendered element tree; it deliberately excludes first-props collection because plain React.memo fails that control assertion. Registry fixtures separately check native/timer cancellation and synchronous registration avoidance.
- The ESLint plugin runs separate ESLint 7 and 9 Jest projects; missing-observer RuleTester fixtures check valid syntax, diagnostics, and exact autofix output. These inspected projects do not establish behavior across every version allowed by its peer range.
- Codemod tests compare applyTransform output with inline snapshots; CLI fixtures write source files and execute subprocesses to check paths containing spaces and mixed TS/TSX parsing, so running these tests would perform filesystem mutations.
- Build/test CI configures Node 24, npm ci --ignore-scripts, lint, build:check, build:test, serial Jest, size reporting, and performance scripts. Coveralls execution/upload are commented out; core Jest discovery selects base tests, and check-size prints measurements without a budget comparison in its inspected run function.
Risk map
These risks are source observations or explicitly labeled hypotheses, not confirmed vulnerabilities. The clearest sensitive boundaries are shell-enabled migration execution, build-time source evaluation, and application-controlled callbacks. Runtime changes have broad effects because global scheduling, dependency tracking, and React cleanup share tightly coupled state.
- Hypothesis: forwarding caller-controlled flags into cp.spawn with shell:true makes shell quoting and metacharacter handling a security-sensitive boundary; the inspected CLI quotes paths but supplies no general argument-escaping layer. (inferred)
- The codemod targets a path derived from the caller's cwd and directory/path arguments, delegating rewriting to jscodeshift; its CLI tests demonstrate actual file modification, rather than confinement to the package checkout.
- The documentation prebuild/prestart hook transpiles errors.ts and evaluates it with Module._compile, then writes docs/errors.md; inspecting or invoking this hook is therefore different from a passive source read.
- enforceActions is a development diagnostic rather than a security boundary: checkIfStateModificationsAreAllowed returns immediately in production and otherwise warns instead of rejecting the write.
- Shared global state and configurable schedulers affect all participating reactions; isolation is rejected during active batches or reaction processing, and disabling error boundaries is explicitly documented in code as preventing recovery.
- internal.ts explicitly warns that bundling order can expose uninitialized classes or variables; production minification also targets names ending in underscores, creating a concrete coupling between internal naming and bundle behavior.
- Observer cleanup depends on closure reachability and subscription timing: useObserver documents that capturing admRef prevents GC and that inlining its administration factory retains render trees; it also explicitly acknowledges remaining cross-component tearing.
- Legacy codemod output has compatibility limits: keepDecorators emits makeObservable(this) without annotations, and observer/inject rewriting remains present, while the MobX 7 migration guide removes legacy decorators and inject. Generated superclass constructors also carry an explicit argument-review warning.
Extension points
Extension mechanisms are explicit interfaces and callback factories rather than a central runtime plugin loader. Observable annotations, enhancers, comparers, scheduling callbacks, and lifecycle hooks allow targeted customization. Package-level registries and shared configuration factories provide corresponding extension boundaries for lint rules and builds.
- Annotation defines make_ and extend_ contracts with MakeResult control flow; annotation maps accept an Annotation, true for the default, or false to skip a property.
- CreateObservableOptions supplies deep, equals, autoBind, name, and defaultDecorator controls; factory creation translates these into enhancer and default-annotation choices.
- createAtom exposes reportObserved/reportChanged and optional observation lifecycle callbacks, allowing custom resources to participate in dependency tracking without adopting an observable container.
- Interceptors form a disposable ordered handler chain; interceptChange snapshots handlers, runs untracked, accepts transformed changes, and stops when a handler cancels with a falsy return.
- Scheduling can be customized per autorun/reaction with scheduler or delay, or globally through configure.reactionScheduler; global replacement composes with the previous scheduler.
- Reaction error handling supports a local constructor handler and a disposable global onReactionError registry; the development tools hook receives spy, getDebugName, and $mobx.
- New ESLint rules plug into the named rules table, with recommended severity configured separately; individual modules expose ESLint create visitors, metadata, diagnostics, and optional fixes.
- Build packages reuse createRollupConfig with input, packageName, packageBase, and globals; the codemod separately exposes a parser and transform with configurable import/decorator/TS parsing behavior.
Contributor handoff
Begin with the owning package and trace its public API into state ownership before editing. Tests and CI were inspected, not executed, and this report is a bounded inventory rather than exhaustive coverage. Future execution should follow the task's authorization and account for build scripts, codemod tests, and documentation generation that write files.
- Read the root and owning-package manifests before selecting commands: workspace aliases are provided, CONTRIBUTING specifies Node 24, and small bug fixes are expected to include a regression test.
- For core edits, first read mobx.ts, internal.ts, and the relevant api/core/types implementation; preserve the documented export-order sensitivity rather than treating internal.ts as an interchangeable barrel.
- Protect initialization invariants: initObservable disables tracking, permits initialization writes in development, batches execution, and restores context; makeAutoObservable caches prototype keys and assumes one initialization without subclassing.
- For React lifecycle work, retain the module-scoped administration factory and read useObserver together with finalization and retention/Suspense fixtures; they explain stable subscriptions, render-tree reachability, and pre-subscription recovery.
- Tests and CI were inspected, not executed. If execution is later authorized, start with a relevant file through the owning package's test script; core test discovery is restricted to base tests, while React and ESLint use distinct project configurations.
- Do not equate root build:check with type checking: it delegates the build:check name with --if-present, while all four inspected library/plugin manifests expose test:types and test:check instead; CI separately invokes build:test.
- Inspect scripts before running them: Rollup empties dist, check-size rebuilds publish artifacts unless --no-build is supplied, docs hooks evaluate source and write Markdown, and codemod CLI fixtures write files and launch subprocesses.
- Use the MobX 7 migration guide to resolve stale API expectations, and preserve repository style: four-space indentation, double quotes, no semicolons or trailing commas, with strict TypeScript checks but noImplicitAny/noImplicitThis disabled.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 126 | 64% |
| Markdown | 71 | docs/config |
| JavaScript | 70 | 36% |
| JSON | 24 | docs/config |
| YAML | 10 | docs/config |
| CSS | 7 | docs/config |
| HTML | 5 | docs/config |
| Shell | 1 | 1% |
Start reading here
Probable source entrypoint: packages/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
packages/ (229 files)
Source layout — packages/
| Path | Files |
|---|---|
packages/eslint-plugin-mobx/ | 26 |
packages/mobx/ | 115 |
packages/mobx-react/ | 38 |
packages/mobx-react-lite/ | 38 |
packages/mobx-undecorate/ | 12 |
Repository structure
| Top-level path | Files |
|---|---|
packages/ | 229 |
docs/ | 74 |
website/ | 12 |
.github/ | 11 |
scripts/ | 3 |
.changeset/ | 3 |
Docs found: CONTRIBUTING.mdREADME.mddocs/LINKS.mddocs/README.mddocs/about-this-documentation.mddocs/actions.mddocs/analyzing-reactivity.mddocs/api.mddocs/backers-sponsors.mddocs/best/debugging-mobx.md
Package
package.json — mobx-root, 48 declared dependencies.
@babel/core@babel/plugin-proposal-class-properties@babel/preset-env@changesets/changelog-github@changesets/cli@rollup/plugin-babel@rollup/plugin-commonjs@rollup/plugin-json@rollup/plugin-node-resolve@rollup/plugin-replace@rollup/plugin-terser@testing-library/dom
CI & config entrypoints
README.md— Repository readme.github/workflows/release.yml— GitHub Actions workflow.github/workflows/coveralls.yml— GitHub Actions workflow.github/workflows/build_and_test.yml— GitHub Actions workflowpackage.json— npm scripts: build, build:check, build:test, check-size, coverage, dedup, docs:build, docs:publishwebsite/package.json— npm scripts: build, examples, prebuild, prestart, publish-gh-pages, rename-version, start, versionpackages/eslint-plugin-mobx/package.json— npm scripts: build, prebuild, prepublishOnly, test, test:7, test:9, test:check, test:typespackages/mobx-react-lite/package.json— npm scripts: build, build:test, lint, prepublishOnly, test, test:check, test:size, test:typespackages/mobx-react/package.json— npm scripts: build, build:test, lint, prepublishOnly, test, test:check, test:size, test:typespackages/mobx-undecorate/package.json— npm scripts: test
Structural health signals
Average score 7.6/10 across 96 analyzed source files; 14 hotspots surfaced.
docs/assets/getting-started-assets/javascripts/JSXTransformer.js— 1808 lines, score 4.7 (Large file, Deep nesting)docs/assets/getting-started-assets/javascripts/codemirror/lib/codemirror.js— 2349 lines, score 5 (Large file, Deep nesting)packages/mobx/src/types/observableobject.ts— 760 lines, score 5.5 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
60be47cab0922026-09-30 — fix: report the boxed value itself in the create spy event (#4716)404c6e08f3ed2026-09-26 — Version Packages (#4714)105d66d735292026-09-25 — refactor: improve observer handling and memory management in tests (#4713)d9f74490822f2026-09-24 — Version Packages (#4711)f78cb16072832026-09-24 — Clean up reactions from suspended observer render (#4709)