Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .changeset/deep-enhancer-primitive-fast-path.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-endbatch-unobservation-reentrancy.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/fix-observableset-receiver-order.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/lazy-observers-allocation.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/use-observer-first-render-retention.md

This file was deleted.

18 changes: 18 additions & 0 deletions packages/mobx-react-lite/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# mobx-react-lite

## 5.0.3

### Patch Changes

- [`6f002b8e543ca39d3ee23681e28848d7f1724fae`](https://github.com/mobxjs/mobx/commit/6f002b8e543ca39d3ee23681e28848d7f1724fae) [#4696](https://github.com/mobxjs/mobx/pull/4696) Thanks [@gesposito](https://github.com/gesposito)! - perf: add a `node` export condition that routes Node and Bun to the existing `dist/index.js` entry, which picks the prebaked development or production CJS build once at require time. `import`ing mobx in Node no longer executes the env-agnostic `dist/mobx.mjs` with per-call `NODE_ENV` checks, and mixing `import` and `require` in one Node app now yields a single mobx instance.

## 5.0.2

### Patch Changes

- [`ec1b708026c1578e1c1f6c7bd90be18b26f7ce85`](https://github.com/mobxjs/mobx/commit/ec1b708026c1578e1c1f6c7bd90be18b26f7ce85) [#4695](https://github.com/mobxjs/mobx/pull/4695) Thanks [@gesposito](https://github.com/gesposito)! - perf: evaluate `NODE_ENV` once at module scope in the env-agnostic esm bundles (`dist/<pkg>.esm.js` and `dist/<pkg>.mjs`) instead of at every `__DEV__` call site. `process.env` is an exotic object in Node, so each check performed a real environment lookup on hot paths; consumers that execute these files as-is (Node ESM, vitest, SSR) see roughly 10x faster observable writes in dev mode. All env-set artifacts and bundler output are unchanged.

## 5.0.1

### Patch Changes

- [`043850ed96266f8bea42bf643a65e62245a98b3c`](https://github.com/mobxjs/mobx/commit/043850ed96266f8bea42bf643a65e62245a98b3c) [#4689](https://github.com/mobxjs/mobx/pull/4689) Thanks [@gesposito](https://github.com/gesposito)! - fix: `useObserver` no longer retains the element tree returned by a component's first render for the component's whole mounted life. `subscribe`/`getSnapshot` were created inside `useObserver`'s first invocation and therefore shared that invocation's closure context with the `reaction.track` callback's captures (`render`, `renderResult`); since React's `useSyncExternalStore` holds `subscribe` while the component is mounted, the first render result (and every fiber and DOM node reachable from it) could never be garbage collected. The administration object is now created by a module-level factory whose scope contains nothing render-related.

## 5.0.0

### Major Changes
Expand Down
8 changes: 6 additions & 2 deletions packages/mobx-react-lite/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobx-react-lite",
"version": "5.0.0",
"version": "5.0.3",
"description": "Lightweight React bindings for MobX based on function components and Hooks",
"source": "src/index.ts",
"type": "commonjs",
Expand All @@ -20,6 +20,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/mobxreactlite.esm.js"
},
"node": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/mobxreactlite.mjs"
Expand Down Expand Up @@ -58,7 +62,7 @@
"react": "^18 || ^19"
},
"devDependencies": {
"mobx": "^7.0.0"
"mobx": "^7.0.3"
},
"keywords": [
"mobx",
Expand Down
18 changes: 18 additions & 0 deletions packages/mobx-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# mobx-react

## 10.0.2

### Patch Changes

- [`6f002b8e543ca39d3ee23681e28848d7f1724fae`](https://github.com/mobxjs/mobx/commit/6f002b8e543ca39d3ee23681e28848d7f1724fae) [#4696](https://github.com/mobxjs/mobx/pull/4696) Thanks [@gesposito](https://github.com/gesposito)! - perf: add a `node` export condition that routes Node and Bun to the existing `dist/index.js` entry, which picks the prebaked development or production CJS build once at require time. `import`ing mobx in Node no longer executes the env-agnostic `dist/mobx.mjs` with per-call `NODE_ENV` checks, and mixing `import` and `require` in one Node app now yields a single mobx instance.

- Updated dependencies [[`6f002b8e543ca39d3ee23681e28848d7f1724fae`](https://github.com/mobxjs/mobx/commit/6f002b8e543ca39d3ee23681e28848d7f1724fae)]:
- mobx-react-lite@5.0.3

## 10.0.1

### Patch Changes

- [`ec1b708026c1578e1c1f6c7bd90be18b26f7ce85`](https://github.com/mobxjs/mobx/commit/ec1b708026c1578e1c1f6c7bd90be18b26f7ce85) [#4695](https://github.com/mobxjs/mobx/pull/4695) Thanks [@gesposito](https://github.com/gesposito)! - perf: evaluate `NODE_ENV` once at module scope in the env-agnostic esm bundles (`dist/<pkg>.esm.js` and `dist/<pkg>.mjs`) instead of at every `__DEV__` call site. `process.env` is an exotic object in Node, so each check performed a real environment lookup on hot paths; consumers that execute these files as-is (Node ESM, vitest, SSR) see roughly 10x faster observable writes in dev mode. All env-set artifacts and bundler output are unchanged.

- Updated dependencies [[`ec1b708026c1578e1c1f6c7bd90be18b26f7ce85`](https://github.com/mobxjs/mobx/commit/ec1b708026c1578e1c1f6c7bd90be18b26f7ce85)]:
- mobx-react-lite@5.0.2

## 10.0.0

### Major Changes
Expand Down
12 changes: 8 additions & 4 deletions packages/mobx-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobx-react",
"version": "10.0.0",
"version": "10.0.2",
"description": "React bindings for MobX. Create fully reactive components.",
"source": "src/index.ts",
"type": "commonjs",
Expand All @@ -20,6 +20,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/mobxreact.esm.js"
},
"node": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/mobxreact.mjs"
Expand Down Expand Up @@ -55,15 +59,15 @@
},
"homepage": "https://mobx.js.org",
"dependencies": {
"mobx-react-lite": "^5.0.0"
"mobx-react-lite": "^5.0.3"
},
"peerDependencies": {
"mobx": "^7.0.0",
"react": "^18 || ^19"
},
"devDependencies": {
"mobx": "^7.0.0",
"mobx-react-lite": "^5.0.0"
"mobx": "^7.0.3",
"mobx-react-lite": "^5.0.3"
},
"keywords": [
"mobx",
Expand Down
30 changes: 30 additions & 0 deletions packages/mobx/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# mobx

## 7.0.3

### Patch Changes

- [`6f002b8e543ca39d3ee23681e28848d7f1724fae`](https://github.com/mobxjs/mobx/commit/6f002b8e543ca39d3ee23681e28848d7f1724fae) [#4696](https://github.com/mobxjs/mobx/pull/4696) Thanks [@gesposito](https://github.com/gesposito)! - perf: add a `node` export condition that routes Node and Bun to the existing `dist/index.js` entry, which picks the prebaked development or production CJS build once at require time. `import`ing mobx in Node no longer executes the env-agnostic `dist/mobx.mjs` with per-call `NODE_ENV` checks, and mixing `import` and `require` in one Node app now yields a single mobx instance.

## 7.0.2

### Patch Changes

- [`ec1b708026c1578e1c1f6c7bd90be18b26f7ce85`](https://github.com/mobxjs/mobx/commit/ec1b708026c1578e1c1f6c7bd90be18b26f7ce85) [#4695](https://github.com/mobxjs/mobx/pull/4695) Thanks [@gesposito](https://github.com/gesposito)! - perf: evaluate `NODE_ENV` once at module scope in the env-agnostic esm bundles (`dist/<pkg>.esm.js` and `dist/<pkg>.mjs`) instead of at every `__DEV__` call site. `process.env` is an exotic object in Node, so each check performed a real environment lookup on hot paths; consumers that execute these files as-is (Node ESM, vitest, SSR) see roughly 10x faster observable writes in dev mode. All env-set artifacts and bundler output are unchanged.

- [`6d8b5fa6c7596d7e5b16e55e3121d81c0c5bb21a`](https://github.com/mobxjs/mobx/commit/6d8b5fa6c7596d7e5b16e55e3121d81c0c5bb21a) [#4672](https://github.com/mobxjs/mobx/pull/4672) Thanks [@chatman-media](https://github.com/chatman-media)! - Fix `ObservableSet.replace` emitting spurious `delete`/`add` events (and triggering reactions) for values that are unchanged. It now only fires `delete` for removed values and `add` for newly added ones, mirroring `ObservableMap.replace`.

Note: because `replace` no longer clears and re-adds every value, the iteration order after `replace` changes in a (subtle but observable) way. Surviving values now keep their original relative position and newly added values are appended, instead of the whole set being reordered to match the argument. For example, `set(["a", "b", "c"]).replace(["d", "b", "a"])` previously iterated as `d, b, a`, and now iterates as `a, b, d`. This is arguably the more correct behavior (unchanged values are genuinely unchanged), but if you relied on `replace` reordering the set to match its argument, you may need to adjust.

## 7.0.1

### Patch Changes

- [`9444c624b957b489875e1c6b45deb290034ce4e9`](https://github.com/mobxjs/mobx/commit/9444c624b957b489875e1c6b45deb290034ce4e9) [#4694](https://github.com/mobxjs/mobx/pull/4694) Thanks [@mrpmohiburrahman](https://github.com/mrpmohiburrahman)! - fix: `onBecomeObserved` is now called for the dependencies of a computed that becomes observed while serving a cached value. Previously, observation only cascaded when the newly observed computed also happened to recompute, so an observable with a live observer chain up to a running reaction could still report itself as unobserved and never fire its hook.

- [`53bb83fdf455a4e606bb721ea10040ded7c57796`](https://github.com/mobxjs/mobx/commit/53bb83fdf455a4e606bb721ea10040ded7c57796) [#4683](https://github.com/mobxjs/mobx/pull/4683) Thanks [@gesposito](https://github.com/gesposito)! - perf: fast-path primitives in `deepEnhancer`. Writing a primitive into a deep observable no longer runs the observable/array/plain-object/Map/Set/function type checks; primitives can never be made observable, so they are returned immediately. Creating an observable array of primitives is ~4x faster, and observable Set/Map writes are ~20-25% faster in the perf suite.

- [`030498d6b2bfe4cc27340fed8c706177cb3b28e1`](https://github.com/mobxjs/mobx/commit/030498d6b2bfe4cc27340fed8c706177cb3b28e1) [#4684](https://github.com/mobxjs/mobx/pull/4684) Thanks [@a-y-ibrahim](https://github.com/a-y-ibrahim)! - Fix a stack overflow ("Maximum call stack size exceeded") that could occur when an `onBecomeUnobserved` handler disposes a `Reaction`. Disposing a `Reaction` re-enters `endBatch()`, which used to recurse into the same `pendingUnobservations` drain loop instead of letting the already-running outer loop pick up the newly queued items, causing unbounded stack depth for long enough chains.

- [`a9086076b9ead1a9c933215bffaa9b57d44f6829`](https://github.com/mobxjs/mobx/commit/a9086076b9ead1a9c933215bffaa9b57d44f6829) [#4681](https://github.com/mobxjs/mobx/pull/4681) Thanks [@spokodev](https://github.com/spokodev)! - Fix ObservableSet union, intersection and symmetricDifference to return results in receiver order, matching native Set, when the argument is a plain Set.

- [`c65a4e14cf48b42cb792dc4c64edbcf56234b32d`](https://github.com/mobxjs/mobx/commit/c65a4e14cf48b42cb792dc4c64edbcf56234b32d) [#4682](https://github.com/mobxjs/mobx/pull/4682) Thanks [@gesposito](https://github.com/gesposito)! - perf: lazily allocate the internal `observers_` Set. Atoms and computed values no longer allocate an empty `Set` upfront; it is created on first observer instead. Most atoms in large stores are never observed, so this saves roughly 160 bytes per unobserved atom (e.g. ~35% lower heap usage when hydrating 50k instances with 10 observable fields each).

## 7.0.0

### Major Changes
Expand Down
45 changes: 45 additions & 0 deletions packages/mobx/__tests__/base/become-observed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,3 +570,48 @@ test("works with ObservableSet #3595", () => {
expect(onSetObserved).toHaveBeenCalledTimes(1)
expect(onSetUnobserved).toHaveBeenCalledTimes(1)
})

test("onBecomeObserved fires when a computed becomes observed while serving a cached value #4547", () => {
const events: string[] = []

const o = observable.box(1)
onBecomeObserved(o, () => events.push("BO"))
onBecomeUnobserved(o, () => events.push("BUO"))
const c = computed(() => o.get())

let disposeAutorun: () => void
runInAction(() => {
// non-reactive read inside the batch leaves `c` up-to-date but unobserved
void c.get()
// `c` becomes observed during endBatch() without recomputing, so it never
// re-reports `o` — the hook has to cascade instead
disposeAutorun = autorun(() => void c.get())
})

expect(events).toEqual(["BO"])

disposeAutorun!()
expect(events).toEqual(["BO", "BUO"])
})

test("onBecomeObserved cascades through a chain of cached computeds #4547", () => {
const events: string[] = []

const o = observable.box(1)
onBecomeObserved(o, () => events.push("BO"))
onBecomeUnobserved(o, () => events.push("BUO"))
// two levels, as in the reported issue: the cascade has to recurse
const inner = computed(() => o.get())
const outer = computed(() => inner.get())

let disposeAutorun: () => void
runInAction(() => {
void outer.get()
disposeAutorun = autorun(() => void outer.get())
})

expect(events).toEqual(["BO"])

disposeAutorun!()
expect(events).toEqual(["BO", "BUO"])
})
106 changes: 106 additions & 0 deletions packages/mobx/__tests__/base/set.js
Original file line number Diff line number Diff line change
Expand Up @@ -523,3 +523,109 @@ describe("Observable Set interceptors", () => {
expect([...s]).toStrictEqual([1, 10])
})
})

describe("#3761 replace only fires events for actual changes", () => {
test("replace only emits delete/add for removed/added values", () => {
const s = set(["a", "b", "c"])
const events = []
mobx.observe(s, change => {
delete change.observableKind
delete change.debugObjectName
events.push(change)
})

// The replacement is intentionally ordered differently from the original
// ("c", "a", "d" vs "a", "b", "c"): "b" is removed, "d" is added, "a"/"c" survive.
s.replace(["c", "a", "d"])

expect(events).toEqual([
{ object: s, oldValue: "b", type: "delete" },
{ object: s, newValue: "d", type: "add" }
])
// Surviving values keep their original relative order ("a" before "c") and the
// added value is appended, so the result iterates as ["a", "c", "d"]. See the
// iteration-order note in the changeset / #3761 discussion.
expect(mobx.values(s)).toEqual(["a", "c", "d"])
})

test("replace with identical content emits no events", () => {
const s = set(["x", "y"])
const events = []
mobx.observe(s, change => events.push(change))

s.replace(["x", "y"])

expect(events).toEqual([])
expect(mobx.values(s)).toEqual(["x", "y"])
})

test("replace with an ES6 Set only emits events for actual changes", () => {
const s = set([1, 2, 3])
const events = []
mobx.observe(s, change => {
delete change.observableKind
delete change.debugObjectName
events.push(change)
})

// Reordered replacement (3, 1, 4 vs 1, 2, 3): 2 is removed, 4 is added, 1/3 survive.
s.replace(new Set([3, 1, 4]))

expect(events).toEqual([
{ object: s, oldValue: 2, type: "delete" },
{ object: s, newValue: 4, type: "add" }
])
// Survivors keep their original relative order (1 before 3), 4 is appended.
expect(mobx.values(s)).toEqual([1, 3, 4])
})

test("replace with an observable Set only emits events for actual changes", () => {
const s = set([1, 2, 3])
const other = set([2, 3, 4])
const events = []
mobx.observe(s, change => {
delete change.observableKind
delete change.debugObjectName
events.push(change)
})

s.replace(other)

expect(events).toEqual([
{ object: s, oldValue: 1, type: "delete" },
{ object: s, newValue: 4, type: "add" }
])
expect(mobx.values(s)).toEqual([2, 3, 4])
})

test("replace with identical content does not report a change", () => {
const s = set([1, 2, 3])
let runCount = 0
const dispose = mobx.autorun(() => {
mobx.values(s)
runCount++
})
expect(runCount).toBe(1)

// Nothing actually changes, so observers must not be notified.
s.replace([1, 2, 3])

expect(runCount).toBe(1)
dispose()
})

test("replace still honors interceptors", () => {
const s = set([1, 2])
mobx.intercept(s, change => {
// Prevent adding 4.
if (change.type === "add" && change.newValue === 4) {
return undefined
}
return change
})

s.replace([2, 3, 4])

expect(mobx.values(s)).toEqual([2, 3])
})
})
6 changes: 5 additions & 1 deletion packages/mobx/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobx",
"version": "7.0.0",
"version": "7.0.3",
"description": "Simple, scalable state management.",
"source": "src/mobx.ts",
"type": "commonjs",
Expand All @@ -20,6 +20,10 @@
"types": "./dist/mobx.d.ts",
"default": "./dist/mobx.esm.js"
},
"node": {
"types": "./dist/mobx.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/mobx.d.ts",
"default": "./dist/mobx.mjs"
Expand Down
Loading