From 8224ab7d99112507220f6ede1e339923733fc523 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 09:51:48 +0300 Subject: [PATCH 01/15] chore(deps): bump coverallsapp/github-action from 2.1.2 to 2.2.0 (#3704) Bumps [coverallsapp/github-action](https://github.com/coverallsapp/github-action) from 2.1.2 to 2.2.0. - [Release notes](https://github.com/coverallsapp/github-action/releases) - [Commits](https://github.com/coverallsapp/github-action/compare/v2.1.2...v2.2.0) --- updated-dependencies: - dependency-name: coverallsapp/github-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/coveralls.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index cf3c310c5..796f1bcd7 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -28,6 +28,6 @@ jobs: run: yarn coverage - name: Upload to coveralls - uses: coverallsapp/github-action@v2.1.2 + uses: coverallsapp/github-action@v2.2.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 27efa3cc637e3195589874990c23d4de82c12072 Mon Sep 17 00:00:00 2001 From: Shahzaib Date: Fri, 23 Jun 2023 17:31:36 +0800 Subject: [PATCH 02/15] Update observable-state.md (#3710) --- docs/observable-state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/observable-state.md b/docs/observable-state.md index c752bc9af..3266d6e68 100644 --- a/docs/observable-state.md +++ b/docs/observable-state.md @@ -283,7 +283,7 @@ const plainMap = new Map(observableMap) ``` To convert a data tree recursively to plain objects, the [`toJS`](api.md#tojs) utility can be used. -For classes, it is recommend to implement a `toJSON()` method, as it will be picked up by `JSON.stringify`. +For classes, it is recommended to implement a `toJSON()` method, as it will be picked up by `JSON.stringify`. ## A short note on classes From 473cb3f5fc8bf43abdd1c9c7857fe2820d2291fe Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 12 Jul 2023 12:48:35 +0100 Subject: [PATCH 03/15] Fix react-strict mode support, remove auto observability of this.props / this.state / this.context in class components (#3718) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * changeset * wip * fix tests * style * wip * wip * wip * wip * wip * wip * wip * wip * fix comment * Added docs * Fixed constructor -> componentDidMount in docs * Fixed failing unit tests? * slightly more robust gc --------- Co-authored-by: Jan Plaček <11457665+urugator@users.noreply.github.com> --- .changeset/flat-pumas-cross.md | 8 + docs/react-integration.md | 2 +- .../__tests__/observer.test.tsx | 20 + ...rentModeUsingFinalizationRegistry.test.tsx | 5 +- .../useAsObservableSource.deprecated.test.tsx | 8 +- .../__tests__/useAsObservableSource.test.tsx | 8 +- .../__tests__/useLocalObservable.test.tsx | 8 +- .../useLocalStore.deprecated.test.tsx | 10 +- packages/mobx-react-lite/src/useLocalStore.ts | 3 +- packages/mobx-react-lite/src/useObserver.ts | 24 +- packages/mobx-react/README.md | 97 ++++- .../__snapshots__/observer.test.tsx.snap | 17 +- packages/mobx-react/__tests__/hooks.test.tsx | 1 + .../mobx-react/__tests__/issue21.test.tsx | 144 ------- .../mobx-react/__tests__/observer.test.tsx | 366 ++++++++++++++---- packages/mobx-react/src/observerClass.ts | 104 ++--- 16 files changed, 491 insertions(+), 334 deletions(-) create mode 100644 .changeset/flat-pumas-cross.md diff --git a/.changeset/flat-pumas-cross.md b/.changeset/flat-pumas-cross.md new file mode 100644 index 000000000..bf45967c4 --- /dev/null +++ b/.changeset/flat-pumas-cross.md @@ -0,0 +1,8 @@ +--- +"mobx-react-lite": patch +"mobx-react": major +--- + +- Fixed `observer` in `StrictMode` #3671 +- **[BREAKING CHANGE]** Class component's `props`/`state`/`context` are no longer observable. Attempt to use these in any derivation other than component's `render` throws and error. For details see https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/README.md#note-on-using-props-and-state-in-derivations +- Extending or applying `observer` classes is now explicitly forbidden diff --git a/docs/react-integration.md b/docs/react-integration.md index af09f8fbd..d241e5bc4 100644 --- a/docs/react-integration.md +++ b/docs/react-integration.md @@ -349,7 +349,7 @@ const TimerView = observer( ) ``` -Check out [mobx-react docs](https://github.com/mobxjs/mobx-react#api-documentation) for more information. +Check out [mobx-react docs](https://github.com/mobxjs/mobx/tree/main/packages/mobx-react#class-components) for more information. diff --git a/packages/mobx-react-lite/__tests__/observer.test.tsx b/packages/mobx-react-lite/__tests__/observer.test.tsx index a889a6f3a..b29d732bf 100644 --- a/packages/mobx-react-lite/__tests__/observer.test.tsx +++ b/packages/mobx-react-lite/__tests__/observer.test.tsx @@ -1087,3 +1087,23 @@ test("Anonymous component displayName #3192", () => { expect(observerError.message).toEqual(memoError.message) consoleErrorSpy.mockRestore() }) + +test("StrictMode #3671", async () => { + const o = mobx.observable({ x: 0 }) + + const Cmp = observer(() => o.x as any) + + const { container, unmount } = render( + + + + ) + + expect(container).toHaveTextContent("0") + act( + mobx.action(() => { + o.x++ + }) + ) + expect(container).toHaveTextContent("1") +}) diff --git a/packages/mobx-react-lite/__tests__/strictAndConcurrentModeUsingFinalizationRegistry.test.tsx b/packages/mobx-react-lite/__tests__/strictAndConcurrentModeUsingFinalizationRegistry.test.tsx index 829450385..c8068d2ac 100644 --- a/packages/mobx-react-lite/__tests__/strictAndConcurrentModeUsingFinalizationRegistry.test.tsx +++ b/packages/mobx-react-lite/__tests__/strictAndConcurrentModeUsingFinalizationRegistry.test.tsx @@ -14,6 +14,7 @@ expect(observerFinalizationRegistry).toBeInstanceOf(globalThis.FinalizationRegis afterEach(cleanup) test("uncommitted components should not leak observations", async () => { + jest.setTimeout(30_000) const store = mobx.observable({ count1: 0, count2: 0 }) // Track whether counts are observed @@ -41,7 +42,9 @@ test("uncommitted components should not leak observations", async () => { ) // Allow gc to kick in in case to let finalization registry cleanup + await new Promise(resolve => setTimeout(resolve, 0)) gc() + await new Promise(resolve => setTimeout(resolve, 0)) // Can take a while (especially on CI) before gc actually calls the registry await waitFor( () => { @@ -51,7 +54,7 @@ test("uncommitted components should not leak observations", async () => { expect(count2IsObserved).toBeFalsy() }, { - timeout: 2000, + timeout: 10_000, interval: 200 } ) diff --git a/packages/mobx-react-lite/__tests__/useAsObservableSource.deprecated.test.tsx b/packages/mobx-react-lite/__tests__/useAsObservableSource.deprecated.test.tsx index 997487dad..46417ca4b 100644 --- a/packages/mobx-react-lite/__tests__/useAsObservableSource.deprecated.test.tsx +++ b/packages/mobx-react-lite/__tests__/useAsObservableSource.deprecated.test.tsx @@ -126,19 +126,19 @@ describe("base useAsObservableSource should work", () => { const { container } = render() expect(container.querySelector("span")!.innerHTML).toBe("10") - expect(counterRender).toBe(1) + expect(counterRender).toBe(2) act(() => { ;(container.querySelector("#inc")! as any).click() }) expect(container.querySelector("span")!.innerHTML).toBe("11") - expect(counterRender).toBe(2) + expect(counterRender).toBe(3) act(() => { ;(container.querySelector("#incmultiplier")! as any).click() }) expect(container.querySelector("span")!.innerHTML).toBe("22") - expect(counterRender).toBe(4) // TODO: should be 3 + expect(counterRender).toBe(5) // TODO: should be 3 }) }) @@ -271,7 +271,7 @@ describe("combining observer with props and stores", () => { store.x = 10 }) - expect(renderedValues).toEqual([10, 15, 15, 20]) // TODO: should have one 15 less + expect(renderedValues).toEqual([10, 10, 15, 15, 20]) // TODO: should have one 15 less // TODO: re-enable this line. When debugging, the correct value is returned from render, // which is also visible with renderedValues, however, querying the dom doesn't show the correct result diff --git a/packages/mobx-react-lite/__tests__/useAsObservableSource.test.tsx b/packages/mobx-react-lite/__tests__/useAsObservableSource.test.tsx index 0f52a206a..3ac397321 100644 --- a/packages/mobx-react-lite/__tests__/useAsObservableSource.test.tsx +++ b/packages/mobx-react-lite/__tests__/useAsObservableSource.test.tsx @@ -192,19 +192,19 @@ describe("base useAsObservableSource should work", () => { const { container } = render() expect(container.querySelector("span")!.innerHTML).toBe("10") - expect(counterRender).toBe(1) + expect(counterRender).toBe(2) act(() => { ;(container.querySelector("#inc")! as any).click() }) expect(container.querySelector("span")!.innerHTML).toBe("11") - expect(counterRender).toBe(2) + expect(counterRender).toBe(3) act(() => { ;(container.querySelector("#incmultiplier")! as any).click() }) expect(container.querySelector("span")!.innerHTML).toBe("22") - expect(counterRender).toBe(4) // TODO: should be 3 + expect(counterRender).toBe(5) // TODO: should be 3 }) }) @@ -337,7 +337,7 @@ describe("combining observer with props and stores", () => { store.x = 10 }) - expect(renderedValues).toEqual([10, 15, 15, 20]) // TODO: should have one 15 less + expect(renderedValues).toEqual([10, 10, 15, 15, 20]) // TODO: should have one 15 less expect(container.querySelector("div")!.textContent).toBe("20") }) diff --git a/packages/mobx-react-lite/__tests__/useLocalObservable.test.tsx b/packages/mobx-react-lite/__tests__/useLocalObservable.test.tsx index 5e9fc592c..f8f592dd4 100644 --- a/packages/mobx-react-lite/__tests__/useLocalObservable.test.tsx +++ b/packages/mobx-react-lite/__tests__/useLocalObservable.test.tsx @@ -116,7 +116,7 @@ describe("is used to keep observable within component body", () => { fireEvent.click(div) expect(div.textContent).toBe("3-2") - expect(renderCount).toBe(3) + expect(renderCount).toBe(4) }) it("actions can be used", () => { @@ -418,19 +418,19 @@ describe("is used to keep observable within component body", () => { const { container } = render() expect(container.querySelector("span")!.innerHTML).toBe("10") - expect(counterRender).toBe(1) + expect(counterRender).toBe(2) act(() => { ;(container.querySelector("#inc")! as any).click() }) expect(container.querySelector("span")!.innerHTML).toBe("11") - expect(counterRender).toBe(2) + expect(counterRender).toBe(3) act(() => { ;(container.querySelector("#incmultiplier")! as any).click() }) expect(container.querySelector("span")!.innerHTML).toBe("22") - expect(counterRender).toBe(4) // TODO: should be 3 + expect(counterRender).toBe(5) // TODO: should be 3 }) }) }) diff --git a/packages/mobx-react-lite/__tests__/useLocalStore.deprecated.test.tsx b/packages/mobx-react-lite/__tests__/useLocalStore.deprecated.test.tsx index d3aa35dc0..297bb0bf7 100644 --- a/packages/mobx-react-lite/__tests__/useLocalStore.deprecated.test.tsx +++ b/packages/mobx-react-lite/__tests__/useLocalStore.deprecated.test.tsx @@ -119,8 +119,8 @@ describe("is used to keep observable within component body", () => { fireEvent.click(div) expect(div.textContent).toBe("3-2") - // though render 3 times, mobx.observable only called once - expect(renderCount).toBe(3) + // though render 4 times, mobx.observable only called once + expect(renderCount).toBe(4) }) it("actions can be used", () => { @@ -424,19 +424,19 @@ describe("is used to keep observable within component body", () => { const { container } = render() expect(container.querySelector("span")!.innerHTML).toBe("10") - expect(counterRender).toBe(1) + expect(counterRender).toBe(2) act(() => { ;(container.querySelector("#inc")! as any).click() }) expect(container.querySelector("span")!.innerHTML).toBe("11") - expect(counterRender).toBe(2) + expect(counterRender).toBe(3) act(() => { ;(container.querySelector("#incmultiplier")! as any).click() }) expect(container.querySelector("span")!.innerHTML).toBe("22") - expect(counterRender).toBe(4) // TODO: should be 3 + expect(counterRender).toBe(5) // TODO: should be 3 }) }) }) diff --git a/packages/mobx-react-lite/src/useLocalStore.ts b/packages/mobx-react-lite/src/useLocalStore.ts index 03f270062..9060ee111 100644 --- a/packages/mobx-react-lite/src/useLocalStore.ts +++ b/packages/mobx-react-lite/src/useLocalStore.ts @@ -13,10 +13,11 @@ export function useLocalStore, TSource extend initializer: (source?: TSource) => TStore, current?: TSource ): TStore { - if ("production" !== process.env.NODE_ENV) + if ("production" !== process.env.NODE_ENV) { useDeprecated( "[mobx-react-lite] 'useLocalStore' is deprecated, use 'useLocalObservable' instead." ) + } const source = current && useAsObservableSource(current) return useState(() => observable(initializer(source), undefined, { autoBind: true }))[0] } diff --git a/packages/mobx-react-lite/src/useObserver.ts b/packages/mobx-react-lite/src/useObserver.ts index 84375c3fc..8685d77cf 100644 --- a/packages/mobx-react-lite/src/useObserver.ts +++ b/packages/mobx-react-lite/src/useObserver.ts @@ -12,7 +12,7 @@ const getServerSnapshot = () => {} // otherwise it will prevent GC and therefore reaction disposal via FinalizationRegistry. type ObserverAdministration = { reaction: Reaction | null // also serves as disposed flag - forceUpdate: Function | null // also serves as mounted flag + onStoreChange: Function | null // also serves as mounted flag // BC: we will use local state version if global isn't available. // It should behave as previous implementation - tearing is still present, // because there is no cross component synchronization, @@ -27,7 +27,7 @@ type ObserverAdministration = { const mobxGlobalState = _getGlobalState() // BC -const globalStateVersionIsAvailable = typeof mobxGlobalState.globalVersion !== "undefined" +const globalStateVersionIsAvailable = typeof mobxGlobalState.stateVersion !== "undefined" function createReaction(adm: ObserverAdministration) { adm.reaction = new Reaction(`observer${adm.name}`, () => { @@ -35,10 +35,10 @@ function createReaction(adm: ObserverAdministration) { // BC adm.stateVersion = Symbol() } - // Force update won't be avaliable until the component "mounts". + // onStoreChange won't be avaliable until the component "mounts". // If state changes in between initial render and mount, // `useSyncExternalStore` should handle that by checking the state version and issuing update. - adm.forceUpdate?.() + adm.onStoreChange?.() }) } @@ -49,28 +49,34 @@ export function useObserver(render: () => T, baseComponentName: string = "obs const admRef = React.useRef(null) + // Provides ability to force component update without changing state version + const [, forceUpdate] = React.useState() + if (!admRef.current) { // First render const adm: ObserverAdministration = { reaction: null, - forceUpdate: null, + onStoreChange: null, stateVersion: Symbol(), name: baseComponentName, subscribe(onStoreChange: () => void) { // Do NOT access admRef here! observerFinalizationRegistry.unregister(adm) - adm.forceUpdate = onStoreChange + adm.onStoreChange = onStoreChange if (!adm.reaction) { - // We've lost our reaction and therefore all subscriptions. + // We've lost our reaction and therefore all subscriptions, occurs when: + // 1. Timer based finalization registry disposed reaction before component mounted. + // 2. React "re-mounts" same component without calling render in between (typically ). // We have to recreate reaction and schedule re-render to recreate subscriptions, // even if state did not change. createReaction(adm) - adm.forceUpdate() + // `onStoreChange` won't force update if subsequent `getSnapshot` returns same value. + forceUpdate(Symbol()) } return () => { // Do NOT access admRef here! - adm.forceUpdate = null + adm.onStoreChange = null adm.reaction?.dispose() adm.reaction = null } diff --git a/packages/mobx-react/README.md b/packages/mobx-react/README.md index 58d93ee94..7115a437a 100644 --- a/packages/mobx-react/README.md +++ b/packages/mobx-react/README.md @@ -15,10 +15,11 @@ This package supports both React and React Native. Only the latest version is actively maintained. If you're missing a fix or a feature in older version, consider upgrading or using [patch-package](https://www.npmjs.com/package/patch-package) -| NPM Version | Support MobX version | Supported React versions | Supports hook based components | +| NPM Version | Support MobX version | Supported React versions | Added support for: | | ----------- | -------------------- | ------------------------ | -------------------------------------------------------------------------------- | -| v7 | 6.\* | >16.8 | Yes | -| v6 | 4.\* / 5.\* | >16.8 <18 | Yes | +| v8 | 6.\* | >16.8 | Hooks, React 18.2 in strict mode | +| v7 | 6.\* | >16.8 < 18.2 | Hooks | +| v6 | 4.\* / 5.\* | >16.8 <18 | Hooks | | v5 | 4.\* / 5.\* | >0.13 <18 | No, but it is possible to use `` sections inside hook based components | mobx-react 6 / 7 is a repackage of the smaller [mobx-react-lite](https://github.com/mobxjs/mobx/tree/main/packages/mobx-react-lite) package + following features from the `mobx-react@5` package added: @@ -57,10 +58,10 @@ Function (and decorator) that converts a React component definition, React compo #### Class Components -When using component classes, `this.props` and `this.state` will be made observables, so the component will react to all changes in props and state that are used by `render`. - `shouldComponentUpdate` is not supported. As such, it is recommended that class components extend `React.PureComponent`. The `observer` will automatically patch non-pure class components with an internal implementation of `React.PureComponent` if necessary. +Extending `observer` class components is not supported. Always apply `observer` only on the last class in the inheritance chain. + See the [MobX](https://mobx.js.org/react-integration.html#react-integration) documentation for more details. ```javascript @@ -87,6 +88,92 @@ class TodoView extends React.Component { const TodoView = observer(({ todo }) =>
{todo.title}
) ``` +##### Note on using props and state in derivations + +`mobx-react` version 6 and lower would automatically turn `this.state` and `this.props` into observables. +This has the benefit that computed properties and reactions were able to observe those. +However, since this pattern is fundamentally incompatible with `StrictMode` in React 18.2 and higher, this behavior has been removed in React 18. + +As a result, we recommend to no longer mark properties as `@computed` in observer components if they depend on `this.state` or `this.props`. + +```javascript +@observer +class Doubler extends React.Component<{ counter: number }> { + @computed // BROKEN! <-- @computed should be removed in mobx-react > 7 + get doubleValue() { + // Changes to this.props will no longer be detected properly, to fix it, + // remove the @computed annotation. + return this.props * 2 + } + + render() { + return
{this.doubleValue}
+ } +} +``` + +Similarly, reactions will no longer respond to `this.state` / `this.props`. This can be overcome by creating an observable copy: + +```javascript +@observer +class Alerter extends React.Component<{ counter: number }> { + @observable observableCounter: number + reactionDisposer + + constructor(props) { + this.observableCounter = counter + } + + componentDidMount() { + // set up a reaction, by observing the observable, + // rather than the prop which is non-reactive: + this.reactionDisposer = autorun(() => { + if (this.observableCounter > 10) { + alert("Reached 10!") + } + }) + } + + componentDidUpdate() { + // sync the observable from props + this.observableCounter = this.props.counter + } + + componentWillUnmount() { + this.reactionDisposer() + } + + render() { + return
{this.props.counter}
+ } +} +``` + +MobX-react will try to detect cases where `this.props`, `this.state` or `this.context` are used by any other derivation than the `render` method of the owning component and throw. +This is to make sure that neither computed properties, nor reactions, nor other components accidentally rely on those fields to be reactive. + +This includes cases where a render callback is passed to a child, that will read from the props or state of a parent component. +As a result, passing a function that might later read a property of a parent in a reactive context will throw as well. +Instead, when using a callback function that is being passed to an `observer` based child, the capture should be captured locally first: + +```javascript +@observer +class ChildWrapper extends React.Component<{ counter: number }> { + render() { + // Collapsible is an observer component that should respond to this.counter, + // if it is expanded + + // BAD: + return

{this.props.counter}

} /> + + // GOOD: (causes to pass down a fresh callback whenever counter changes, + // that doesn't depend on its parents props) + const counter = this.props.counter + return

{counter}

} /> + } +} +``` + ### `Observer` `Observer` is a React component, which applies `observer` to an anonymous region in your component. diff --git a/packages/mobx-react/__tests__/__snapshots__/observer.test.tsx.snap b/packages/mobx-react/__tests__/__snapshots__/observer.test.tsx.snap index 3c972cb79..78c26f6b9 100644 --- a/packages/mobx-react/__tests__/__snapshots__/observer.test.tsx.snap +++ b/packages/mobx-react/__tests__/__snapshots__/observer.test.tsx.snap @@ -2,22 +2,7 @@ exports[`#3492 should not cause warning by calling forceUpdate on uncommited components 1`] = `[MockFunction]`; -exports[`Redeclaring an existing observer component as an observer should log a warning 1`] = ` -[MockFunction] { - "calls": Array [ - Array [ - "The provided component class (AlreadyObserver) - has already been declared as an observer component.", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], -} -`; +exports[`Redeclaring an existing observer component as an observer should throw 1`] = `"The provided component class (AlreadyObserver) has already been declared as an observer component."`; exports[`SSR works #3448 1`] = `[MockFunction]`; diff --git a/packages/mobx-react/__tests__/hooks.test.tsx b/packages/mobx-react/__tests__/hooks.test.tsx index 3b93654ab..0b161a181 100644 --- a/packages/mobx-react/__tests__/hooks.test.tsx +++ b/packages/mobx-react/__tests__/hooks.test.tsx @@ -93,6 +93,7 @@ test("computed properties result in double render when using observer instead of expect(seen).toEqual([ "parent", 0, + 0, "parent", 2, 2 // should contain "2" only once! But with hooks, one update is scheduled based the fact that props change, the other because the observable source changed. diff --git a/packages/mobx-react/__tests__/issue21.test.tsx b/packages/mobx-react/__tests__/issue21.test.tsx index cd23e77d7..99c8c3d79 100644 --- a/packages/mobx-react/__tests__/issue21.test.tsx +++ b/packages/mobx-react/__tests__/issue21.test.tsx @@ -212,113 +212,6 @@ test("verify prop changes are picked up", () => { expect(container.textContent).toMatchInlineSnapshot(`"1.2.test.0"`) }) -test("verify props is reactive", () => { - function createItem(subid, label) { - const res = observable( - { - subid, - id: 1, - label: label, - get text() { - events.push(["compute", this.subid]) - return ( - this.id + - "." + - this.subid + - "." + - this.label + - "." + - data.items.indexOf(this as any) - ) - } - }, - {}, - { proxy: false } - ) - res.subid = subid // non reactive - return res - } - - const data = observable({ - items: [createItem(1, "hi")] - }) - const events: Array = [] - - class Child extends React.Component { - constructor(p) { - super(p) - makeObservable(this) - } - - @computed - get computedLabel() { - events.push(["computed label", this.props.item.subid]) - return this.props.item.label - } - componentDidMount() { - events.push(["mount"]) - } - componentDidUpdate(prevProps) { - events.push(["update", prevProps.item.subid, this.props.item.subid]) - } - render() { - events.push(["render", this.props.item.subid, this.props.item.text, this.computedLabel]) - return ( - - {this.props.item.text} - {this.computedLabel} - - ) - } - } - - const ChildAsObserver = observer(Child) - - const Parent = observer( - class Parent extends React.Component { - render() { - return ( -
- {data.items.map(item => ( - - ))} -
- ) - } - } - ) - - const Wrapper = () => - - function changeStuff() { - act(() => { - transaction(() => { - // components start rendeirng a new item, but computed is still based on old value - data.items = [createItem(2, "test")] - }) - }) - } - - const { container } = render() - expect(events.sort()).toEqual( - [["mount"], ["compute", 1], ["computed label", 1], ["render", 1, "1.1.hi.0", "hi"]].sort() - ) - - events.splice(0) - let testDiv = container.querySelector("#testDiv") as HTMLElement - testDiv.click() - - expect(events.sort()).toEqual( - [ - ["compute", 1], - ["update", 1, 2], - ["compute", 2], - ["computed label", 2], - ["render", 2, "1.2.test.0", "test"] - ].sort() - ) -}) - test("no re-render for shallow equal props", async () => { function createItem(subid, label) { const res = observable({ @@ -415,40 +308,3 @@ test("lifecycle callbacks called with correct arguments", () => { let testButton = container.querySelector("#testButton") as HTMLElement testButton.click() }) - -test("verify props are reactive in constructor", () => { - const propValues: Array = [] - let constructorCallsCount = 0 - - const Component = observer( - class Component extends React.Component { - disposer: IReactionDisposer - constructor(props, context) { - super(props, context) - constructorCallsCount++ - this.disposer = reaction( - () => this.props.prop, - prop => propValues.push(prop), - { - fireImmediately: true - } - ) - } - - componentWillUnmount() { - this.disposer() - } - - render() { - return
- } - } - ) - - const { rerender } = render() - rerender() - rerender() - rerender() - expect(constructorCallsCount).toEqual(1) - expect(propValues).toEqual(["1", "2", "3", "4"]) -}) diff --git a/packages/mobx-react/__tests__/observer.test.tsx b/packages/mobx-react/__tests__/observer.test.tsx index cc5d818d8..864085813 100644 --- a/packages/mobx-react/__tests__/observer.test.tsx +++ b/packages/mobx-react/__tests__/observer.test.tsx @@ -1,16 +1,22 @@ -import React, { createContext, Fragment, StrictMode, Suspense } from "react" +import React, { StrictMode, Suspense } from "react" import { inject, observer, Observer, enableStaticRendering } from "../src" import { render, act, waitFor } from "@testing-library/react" import { getObserverTree, + _getGlobalState, _resetGlobalState, action, computed, observable, transaction, - makeObservable + makeObservable, + autorun, + IReactionDisposer, + reaction, + configure } from "mobx" import { withConsole } from "./utils/withConsole" +import { shallowEqual } from "../src/utils/utils" /** * some test suite is too tedious */ @@ -871,9 +877,7 @@ test.skip("#709 - applying observer on React.memo component", () => { render(, { wrapper: ErrorCatcher }) }) -test("Redeclaring an existing observer component as an observer should log a warning", () => { - consoleWarnMock = jest.spyOn(console, "warn").mockImplementation(() => {}) - +test("Redeclaring an existing observer component as an observer should throw", () => { @observer class AlreadyObserver extends React.Component { render() { @@ -881,8 +885,7 @@ test("Redeclaring an existing observer component as an observer should log a war } } - observer(AlreadyObserver) - expect(consoleWarnMock).toMatchSnapshot() + expect(() => observer(AlreadyObserver)).toThrowErrorMatchingSnapshot() }) test("Missing render should throw", () => { @@ -894,74 +897,6 @@ test("Missing render should throw", () => { expect(() => observer(Component)).toThrow() }) -test("this.context is observable if ComponentName.contextType is set", () => { - const Context = createContext({}) - - let renderCounter = 0 - - @observer - class Parent extends React.Component { - constructor(props: any) { - super(props) - makeObservable(this) - } - - @observable - counter = 0 - - @computed - get contextValue() { - return { counter: this.counter } - } - - render() { - return ( - {this.props.children} - ) - } - } - - @observer - class Child extends React.Component { - static contextType = Context - - constructor(props: any) { - super(props) - makeObservable(this) - } - - @computed - get counterValue() { - return (this.context as any).counter - } - - render() { - renderCounter++ - return
{this.counterValue}
- } - } - - const parentRef = React.createRef() - - const app = ( - - - - ) - - const { unmount, container } = render(app) - - act(() => { - if (parentRef.current) { - parentRef.current!.counter = 1 - } - }) - - expect(renderCounter).toBe(2) - expect(container).toHaveTextContent("1") - unmount() -}) - test("class observer supports re-mounting #3395", () => { const state = observable.box(1) let mountCounter = 0 @@ -1076,3 +1011,284 @@ test("#3492 should not cause warning by calling forceUpdate on uncommited compon unmount() expect(consoleWarnMock).toMatchSnapshot() }) +;["props", "state", "context"].forEach(key => { + test(`using ${key} in computed throws`, () => { + // React prints errors even if we catch em + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}) + + const TestCmp = observer( + class TestCmp extends React.Component { + render() { + computed(() => this[key]).get() + return "" + } + } + ) + + expect(() => render()).toThrowError( + new RegExp(`^\\[mobx-react\\] Cannot read "TestCmp.${key}" in a reactive context`) + ) + + consoleErrorSpy.mockRestore() + }) + + test(`using ${key} in autorun throws`, () => { + // React prints errors even if we catch em + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}) + + let caughtError + + const TestCmp = observer( + class TestCmp extends React.Component { + disposeAutorun: IReactionDisposer | undefined + + componentDidMount(): void { + this.disposeAutorun = autorun(() => this[key], { + onError: error => (caughtError = error) + }) + } + + componentWillUnmount(): void { + this.disposeAutorun?.() + } + + render() { + return "" + } + } + ) + + render() + expect(caughtError?.message).toMatch( + new RegExp(`^\\[mobx-react\\] Cannot read "TestCmp.${key}" in a reactive context`) + ) + + consoleErrorSpy.mockRestore() + }) +}) + +test(`Component react's to observable changes in componenDidMount #3691`, () => { + const o = observable.box(0) + + const TestCmp = observer( + class TestCmp extends React.Component { + componentDidMount(): void { + o.set(o.get() + 1) + } + + render() { + return o.get() + } + } + ) + + const { container, unmount } = render() + expect(container).toHaveTextContent("1") + unmount() +}) + +test(`Observable changes in componenWillUnmount don't cause any warnings or errors`, () => { + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}) + const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}) + const o = observable.box(0) + + const TestCmp = observer( + class TestCmp extends React.Component { + componentWillUnmount(): void { + o.set(o.get() + 1) + } + + render() { + return o.get() + } + } + ) + + const { container, unmount } = render() + expect(container).toHaveTextContent("0") + unmount() + + expect(consoleErrorSpy).not.toBeCalled() + expect(consoleWarnSpy).not.toBeCalled() + + consoleErrorSpy.mockRestore() + consoleWarnSpy.mockRestore() +}) + +test(`Observable prop workaround`, () => { + configure({ + enforceActions: "observed" + }) + + const propValues: Array = [] + + const TestCmp = observer( + class TestCmp extends React.Component<{ prop: number }> { + disposeReaction: IReactionDisposer | undefined + observableProp: number + + get computed() { + return this.observableProp + 100 + } + + constructor(props) { + super(props) + // Synchronize our observableProp with the actual prop on the first render. + this.observableProp = this.props.prop + makeObservable(this, { + observableProp: observable, + computed: computed, + // Mutates observable therefore must be action + componentDidUpdate: action + }) + } + + componentDidMount(): void { + // Reactions/autoruns must be created in componenDidMount (not in constructor). + this.disposeReaction = reaction( + () => this.observableProp, + prop => propValues.push(prop), + { + fireImmediately: true + } + ) + } + + componentDidUpdate(): void { + // Synchronize our observableProp with the actual prop on every update. + this.observableProp = this.props.prop + } + + componentWillUnmount(): void { + this.disposeReaction?.() + } + + render() { + return this.computed + } + } + ) + + const { container, unmount, rerender } = render() + expect(container).toHaveTextContent("101") + rerender() + expect(container).toHaveTextContent("102") + rerender() + expect(container).toHaveTextContent("103") + rerender() + expect(container).toHaveTextContent("104") + expect(propValues).toEqual([1, 2, 3, 4]) + unmount() +}) + +test(`Observable props/state/context workaround`, () => { + configure({ + enforceActions: "observed" + }) + + const reactionResults: Array = [] + + const ContextType = React.createContext(0) + + const TestCmp = observer( + class TestCmp extends React.Component { + static contextType = ContextType + + disposeReaction: IReactionDisposer | undefined + observableProps: any + observableState: any + observableContext: any + + constructor(props, context) { + super(props, context) + this.state = { + x: 0 + } + this.observableState = this.state + this.observableProps = this.props + this.observableContext = this.context + makeObservable(this, { + observableProps: observable, + observableState: observable, + observableContext: observable, + computed: computed, + componentDidUpdate: action + }) + } + + get computed() { + return `${this.observableProps?.x}${this.observableState?.x}${this.observableContext}` + } + + componentDidMount(): void { + this.disposeReaction = reaction( + () => this.computed, + prop => reactionResults.push(prop), + { + fireImmediately: true + } + ) + } + + componentDidUpdate(): void { + // Props are different object with every update + if (!shallowEqual(this.observableProps, this.props)) { + this.observableProps = this.props + } + if (!shallowEqual(this.observableState, this.state)) { + this.observableState = this.state + } + if (!shallowEqual(this.observableContext, this.context)) { + this.observableContext = this.context + } + } + + componentWillUnmount(): void { + this.disposeReaction?.() + } + + render() { + return ( + this.setState(state => ({ x: state.x + 1 }))} + > + {this.computed} + + ) + } + } + ) + + const App = () => { + const [context, setContext] = React.useState(0) + const [prop, setProp] = React.useState(0) + return ( + + setContext(val => val + 1)}> + setProp(val => val + 1)}> + + + ) + } + + const { container, unmount } = render() + + const updateProp = () => + act(() => (container.querySelector("#updateProp") as HTMLElement).click()) + const updateState = () => + act(() => (container.querySelector("#updateState") as HTMLElement).click()) + const updateContext = () => + act(() => (container.querySelector("#updateContext") as HTMLElement).click()) + + expect(container).toHaveTextContent("000") + updateProp() + expect(container).toHaveTextContent("100") + updateState() + expect(container).toHaveTextContent("110") + updateContext() + expect(container).toHaveTextContent("111") + + expect(reactionResults).toEqual(["000", "100", "110", "111"]) + unmount() +}) diff --git a/packages/mobx-react/src/observerClass.ts b/packages/mobx-react/src/observerClass.ts index dd91434a6..d4fb346ce 100644 --- a/packages/mobx-react/src/observerClass.ts +++ b/packages/mobx-react/src/observerClass.ts @@ -1,12 +1,10 @@ import { PureComponent, Component, ComponentClass, ClassAttributes } from "react" import { - createAtom, _allowStateChanges, Reaction, _allowStateReadsStart, _allowStateReadsEnd, - _getGlobalState, - IAtom + _getGlobalState } from "mobx" import { isUsingStaticRendering, @@ -17,21 +15,24 @@ import { shallowEqual, patch } from "./utils/utils" const administrationSymbol = Symbol("ObserverAdministration") const isMobXReactObserverSymbol = Symbol("isMobXReactObserver") +let observablePropDescriptors: PropertyDescriptorMap +if (__DEV__) { + observablePropDescriptors = { + props: createObservablePropDescriptor("props"), + state: createObservablePropDescriptor("state"), + context: createObservablePropDescriptor("context") + } +} + type ObserverAdministration = { reaction: Reaction | null // also serves as disposed flag forceUpdate: Function | null mounted: boolean // we could use forceUpdate as mounted flag name: string - propsAtom: IAtom - stateAtom: IAtom - contextAtom: IAtom + // Used only on __DEV__ props: any state: any context: any - // Setting this.props causes forceUpdate, because this.props is observable. - // forceUpdate sets this.props. - // This flag is used to avoid the loop. - isUpdating: boolean } function getAdministration(component: Component): ObserverAdministration { @@ -45,11 +46,7 @@ function getAdministration(component: Component): ObserverAdministration { name: getDisplayName(component.constructor as ComponentClass), state: undefined, props: undefined, - context: undefined, - propsAtom: createAtom("props"), - stateAtom: createAtom("state"), - contextAtom: createAtom("context"), - isUpdating: false + context: undefined }) } @@ -60,9 +57,8 @@ export function makeClassComponentObserver( if (componentClass[isMobXReactObserverSymbol]) { const displayName = getDisplayName(componentClass) - console.warn( - `The provided component class (${displayName}) - has already been declared as an observer component.` + throw new Error( + `The provided component class (${displayName}) has already been declared as an observer component.` ) } else { componentClass[isMobXReactObserverSymbol] = true @@ -82,15 +78,9 @@ export function makeClassComponentObserver( } } - // this.props and this.state are made observable, just to make sure @computed fields that - // are defined inside the component, and which rely on state or props, re-compute if state or props change - // (otherwise the computed wouldn't update and become stale on props change, since props are not observable) - // However, this solution is not without it's own problems: https://github.com/mobxjs/mobx-react/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aobservable-props-or-not+ - Object.defineProperties(prototype, { - props: observablePropsDescriptor, - state: observableStateDescriptor, - context: observableContextDescriptor - }) + if (__DEV__) { + Object.defineProperties(prototype, observablePropDescriptors) + } const originalRender = prototype.render if (typeof originalRender !== "function") { @@ -114,7 +104,16 @@ export function makeClassComponentObserver( return this.render() } - patch(prototype, "componentDidMount", function () { + const originalComponentDidMount = prototype.componentDidMount + prototype.componentDidMount = function () { + if (__DEV__ && this.componentDidMount !== Object.getPrototypeOf(this).componentDidMount) { + const displayName = getDisplayName(componentClass) + throw new Error( + `[mobx-react] \`observer(${displayName}).componentDidMount\` must be defined on prototype.` + + `\n\`componentDidMount = () => {}\` or \`componentDidMount = function() {}\` is not supported.` + ) + } + // `componentDidMount` may not be called at all. React can abandon the instance after `render`. // That's why we use finalization registry to dispose reaction created during render. // Happens with `` see #3492 @@ -148,8 +147,10 @@ export function makeClassComponentObserver( // The reaction will be created lazily by following render. admin.forceUpdate() } - }) + return originalComponentDidMount?.apply(this, arguments) + } + // TODO@major Overly complicated "patch" is only needed to support the deprecated @disposeOnUnmount patch(prototype, "componentWillUnmount", function () { if (isUsingStaticRendering()) { return @@ -207,12 +208,6 @@ function createReactiveRender(originalRender: any) { function createReaction(admin: ObserverAdministration) { return new Reaction(`${admin.name}.render()`, () => { - if (admin.isUpdating) { - // Reaction is suppressed when setting new state/props/context, - // this is when component is already being updated. - return - } - if (!admin.mounted) { // This is neccessary to avoid react warning about calling forceUpdate on component that isn't mounted yet. // This happens when component is abandoned after render - our reaction is already created and reacts to changes. @@ -221,14 +216,10 @@ function createReaction(admin: ObserverAdministration) { } try { - // forceUpdate sets new `props`, since we made it observable, it would `reportChanged`, causing a loop. - admin.isUpdating = true admin.forceUpdate?.() } catch (error) { admin.reaction?.dispose() admin.reaction = null - } finally { - admin.isUpdating = false } }) } @@ -251,40 +242,23 @@ function observerSCU(nextProps: ClassAttributes, nextState: any): boolean { } function createObservablePropDescriptor(key: "props" | "state" | "context") { - const atomKey = `${key}Atom` return { configurable: true, enumerable: true, get() { const admin = getAdministration(this) - - let prevReadState = _allowStateReadsStart(true) - - admin[atomKey].reportObserved() - - _allowStateReadsEnd(prevReadState) - + const derivation = _getGlobalState().trackingDerivation + if (derivation && derivation !== admin.reaction) { + throw new Error( + `[mobx-react] Cannot read "${admin.name}.${key}" in a reactive context, as it isn't observable. + Please use component lifecycle method to copy the value into a local observable first. + See https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/README.md#note-on-using-props-and-state-in-derivations` + ) + } return admin[key] }, set(value) { - const admin = getAdministration(this) - // forceUpdate issued by reaction sets new props. - // It sets isUpdating to true to prevent loop. - if (!admin.isUpdating && !shallowEqual(admin[key], value)) { - admin[key] = value - // This notifies all observers including our component, - // but we don't want to cause `forceUpdate`, because component is already updating, - // therefore supress component reaction. - admin.isUpdating = true - admin[atomKey].reportChanged() - admin.isUpdating = false - } else { - admin[key] = value - } + getAdministration(this)[key] = value } } } - -const observablePropsDescriptor = createObservablePropDescriptor("props") -const observableStateDescriptor = createObservablePropDescriptor("state") -const observableContextDescriptor = createObservablePropDescriptor("context") From a866dcaca2ac737588ca2e9e93e6ceb63095359f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 18:52:20 +0100 Subject: [PATCH 04/15] Version release (#3672) Co-authored-by: github-actions[bot] --- .changeset/eighty-feet-peel.md | 5 ----- .changeset/flat-pumas-cross.md | 8 -------- packages/mobx-react-lite/CHANGELOG.md | 10 ++++++++++ packages/mobx-react-lite/package.json | 2 +- packages/mobx-react/CHANGELOG.md | 13 +++++++++++++ packages/mobx-react/package.json | 6 +++--- 6 files changed, 27 insertions(+), 17 deletions(-) delete mode 100644 .changeset/eighty-feet-peel.md delete mode 100644 .changeset/flat-pumas-cross.md diff --git a/.changeset/eighty-feet-peel.md b/.changeset/eighty-feet-peel.md deleted file mode 100644 index 3cc3cb486..000000000 --- a/.changeset/eighty-feet-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"mobx-react-lite": patch ---- - -fix #3669: SSR: `useSyncExternalStore` throws due to missing `getServerSnapshot` diff --git a/.changeset/flat-pumas-cross.md b/.changeset/flat-pumas-cross.md deleted file mode 100644 index bf45967c4..000000000 --- a/.changeset/flat-pumas-cross.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"mobx-react-lite": patch -"mobx-react": major ---- - -- Fixed `observer` in `StrictMode` #3671 -- **[BREAKING CHANGE]** Class component's `props`/`state`/`context` are no longer observable. Attempt to use these in any derivation other than component's `render` throws and error. For details see https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/README.md#note-on-using-props-and-state-in-derivations -- Extending or applying `observer` classes is now explicitly forbidden diff --git a/packages/mobx-react-lite/CHANGELOG.md b/packages/mobx-react-lite/CHANGELOG.md index 103e203a2..0233b359d 100644 --- a/packages/mobx-react-lite/CHANGELOG.md +++ b/packages/mobx-react-lite/CHANGELOG.md @@ -1,5 +1,15 @@ # mobx-react-lite +## 4.0.3 + +### Patch Changes + +- [`58bb052c`](https://github.com/mobxjs/mobx/commit/58bb052ca41b8592e5bd5c3003b68ec52da53f33) [#3670](https://github.com/mobxjs/mobx/pull/3670) Thanks [@urugator](https://github.com/urugator)! - fix #3669: SSR: `useSyncExternalStore` throws due to missing `getServerSnapshot` + +* [`473cb3f5`](https://github.com/mobxjs/mobx/commit/473cb3f5fc8bf43abdd1c9c7857fe2820d2291fe) [#3718](https://github.com/mobxjs/mobx/pull/3718) Thanks [@mweststrate](https://github.com/mweststrate)! - - Fixed `observer` in `StrictMode` #3671 + - **[BREAKING CHANGE]** Class component's `props`/`state`/`context` are no longer observable. Attempt to use these in any derivation other than component's `render` throws and error. For details see https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/README.md#note-on-using-props-and-state-in-derivations + - Extending or applying `observer` classes is now explicitly forbidden + ## 4.0.2 ### Patch Changes diff --git a/packages/mobx-react-lite/package.json b/packages/mobx-react-lite/package.json index 1179cf07a..34aef04ff 100644 --- a/packages/mobx-react-lite/package.json +++ b/packages/mobx-react-lite/package.json @@ -1,6 +1,6 @@ { "name": "mobx-react-lite", - "version": "4.0.2", + "version": "4.0.3", "description": "Lightweight React bindings for MobX based on React 16.8+ and Hooks", "source": "src/index.ts", "main": "dist/index.js", diff --git a/packages/mobx-react/CHANGELOG.md b/packages/mobx-react/CHANGELOG.md index 9d6808242..82012d83c 100644 --- a/packages/mobx-react/CHANGELOG.md +++ b/packages/mobx-react/CHANGELOG.md @@ -1,5 +1,18 @@ # mobx-react +## 9.0.0 + +### Major Changes + +- [`473cb3f5`](https://github.com/mobxjs/mobx/commit/473cb3f5fc8bf43abdd1c9c7857fe2820d2291fe) [#3718](https://github.com/mobxjs/mobx/pull/3718) Thanks [@mweststrate](https://github.com/mweststrate)! - - Fixed `observer` in `StrictMode` #3671 + - **[BREAKING CHANGE]** Class component's `props`/`state`/`context` are no longer observable. Attempt to use these in any derivation other than component's `render` throws and error. For details see https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/README.md#note-on-using-props-and-state-in-derivations + - Extending or applying `observer` classes is now explicitly forbidden + +### Patch Changes + +- Updated dependencies [[`58bb052c`](https://github.com/mobxjs/mobx/commit/58bb052ca41b8592e5bd5c3003b68ec52da53f33), [`473cb3f5`](https://github.com/mobxjs/mobx/commit/473cb3f5fc8bf43abdd1c9c7857fe2820d2291fe)]: + - mobx-react-lite@4.0.3 + ## 8.0.0 ### Major Changes diff --git a/packages/mobx-react/package.json b/packages/mobx-react/package.json index 1aace2145..a3edb044d 100644 --- a/packages/mobx-react/package.json +++ b/packages/mobx-react/package.json @@ -1,6 +1,6 @@ { "name": "mobx-react", - "version": "8.0.0", + "version": "9.0.0", "description": "React bindings for MobX. Create fully reactive components.", "source": "src/index.ts", "main": "dist/index.js", @@ -36,7 +36,7 @@ }, "homepage": "https://mobx.js.org", "dependencies": { - "mobx-react-lite": "^4.0.0" + "mobx-react-lite": "^4.0.3" }, "peerDependencies": { "mobx": "^6.9.0", @@ -52,7 +52,7 @@ }, "devDependencies": { "mobx": "^6.9.0", - "mobx-react-lite": "^4.0.0", + "mobx-react-lite": "^4.0.3", "expose-gc": "^1.0.0" }, "keywords": [ From 235c5f17f17124aa609f5f7069f5d7acf07942b8 Mon Sep 17 00:00:00 2001 From: Daniel Meyer <8926560+pubkey@users.noreply.github.com> Date: Wed, 12 Jul 2023 20:01:11 +0200 Subject: [PATCH 05/15] docs: ADD RxDB sponsor (#3719) --- docs/README.md | 1 + docs/assets/rxdb.png | Bin 0 -> 5887 bytes 2 files changed, 1 insertion(+) create mode 100644 docs/assets/rxdb.png diff --git a/docs/README.md b/docs/README.md index dcca5b713..689f68847 100644 --- a/docs/README.md +++ b/docs/README.md @@ -42,6 +42,7 @@ MobX is made possible by the generosity of the sponsors below, and many other [i Casino Sites UPPER EaseUS +RxDB JavaScript Database **🥉 Bronze sponsors (\$500+ total contributions):**
Bugsnag diff --git a/docs/assets/rxdb.png b/docs/assets/rxdb.png new file mode 100644 index 0000000000000000000000000000000000000000..46762a81e5038bb3dbf41974d5e4e036f8c9c479 GIT binary patch literal 5887 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 Z-w{a~*Bn`0Df(|iOHKc2rHXCD{{RAP_4@z- literal 0 HcmV?d00001 From 4792303ec9119c1ba54134fff7e845d21a1d9337 Mon Sep 17 00:00:00 2001 From: Egor Gorbachev <7gorbachevm@gmail.com> Date: Wed, 12 Jul 2023 21:04:47 +0300 Subject: [PATCH 06/15] fix: Make trace() noop in production build (#3709) --- .changeset/fifty-rivers-doubt.md | 5 +++++ packages/mobx/src/api/trace.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/fifty-rivers-doubt.md diff --git a/.changeset/fifty-rivers-doubt.md b/.changeset/fifty-rivers-doubt.md new file mode 100644 index 000000000..19562ef95 --- /dev/null +++ b/.changeset/fifty-rivers-doubt.md @@ -0,0 +1,5 @@ +--- +"mobx": patch +--- + +Make trace() noop in production build diff --git a/packages/mobx/src/api/trace.ts b/packages/mobx/src/api/trace.ts index 297014f54..c304329b3 100644 --- a/packages/mobx/src/api/trace.ts +++ b/packages/mobx/src/api/trace.ts @@ -5,7 +5,7 @@ export function trace(thing?: any, enterBreakPoint?: boolean): void export function trace(enterBreakPoint?: boolean): void export function trace(...args: any[]): void { if (!__DEV__) { - die(`trace() is not available in production builds`) + return } let enterBreakPoint = false if (typeof args[args.length - 1] === "boolean") { From e8ec0e11f026a56096afad96cfa81c55bccd0d9f Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 12 Jul 2023 20:27:20 +0200 Subject: [PATCH 07/15] chore: fixes in release job and readme --- .github/workflows/release.yml | 4 ++-- packages/mobx-react/README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f430277ad..62f9bc88e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repo - uses: actions/checkout@master + uses: actions/checkout@main with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 @@ -34,4 +34,4 @@ jobs: title: Next release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/packages/mobx-react/README.md b/packages/mobx-react/README.md index 7115a437a..bcb2f8d3e 100644 --- a/packages/mobx-react/README.md +++ b/packages/mobx-react/README.md @@ -17,8 +17,8 @@ Only the latest version is actively maintained. If you're missing a fix or a fea | NPM Version | Support MobX version | Supported React versions | Added support for: | | ----------- | -------------------- | ------------------------ | -------------------------------------------------------------------------------- | -| v8 | 6.\* | >16.8 | Hooks, React 18.2 in strict mode | -| v7 | 6.\* | >16.8 < 18.2 | Hooks | +| v9 | 6.\* | >16.8 | Hooks, React 18.2 in strict mode | +| v7,v8 | 6.\* | >16.8 < 18.2 | Hooks | | v6 | 4.\* / 5.\* | >16.8 <18 | Hooks | | v5 | 4.\* / 5.\* | >0.13 <18 | No, but it is possible to use `` sections inside hook based components | From 278323ef234a42eb224a7716f89871e438c3e6cf Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 12 Jul 2023 20:28:38 +0200 Subject: [PATCH 08/15] chore: `prepublish` -> `prepublishOnly` --- packages/eslint-plugin-mobx/package.json | 2 +- packages/mobx-react-lite/package.json | 2 +- packages/mobx-react/README.md | 2 +- packages/mobx-react/package.json | 2 +- packages/mobx/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin-mobx/package.json b/packages/eslint-plugin-mobx/package.json index a146aac00..9f4b90fa4 100644 --- a/packages/eslint-plugin-mobx/package.json +++ b/packages/eslint-plugin-mobx/package.json @@ -47,6 +47,6 @@ "scripts": { "test": "jest", "build": "yarn rollup --config", - "prepublish": "yarn build" + "prepublishOnly": "yarn build" } } diff --git a/packages/mobx-react-lite/package.json b/packages/mobx-react-lite/package.json index 34aef04ff..a9212a577 100644 --- a/packages/mobx-react-lite/package.json +++ b/packages/mobx-react-lite/package.json @@ -76,6 +76,6 @@ "test:size": "yarn import-size --report . observer useLocalObservable", "test:types": "tsc --noEmit", "test:check": "yarn test:types", - "prepublish": "cd ../mobx && yarn build --target publish && cd ../mobx-react-lite && yarn build --target publish && yarn build:cjs && yarn build:es" + "prepublishOnly": "cd ../mobx && yarn build --target publish && cd ../mobx-react-lite && yarn build --target publish && yarn build:cjs && yarn build:es" } } diff --git a/packages/mobx-react/README.md b/packages/mobx-react/README.md index bcb2f8d3e..bb709c8b3 100644 --- a/packages/mobx-react/README.md +++ b/packages/mobx-react/README.md @@ -18,7 +18,7 @@ Only the latest version is actively maintained. If you're missing a fix or a fea | NPM Version | Support MobX version | Supported React versions | Added support for: | | ----------- | -------------------- | ------------------------ | -------------------------------------------------------------------------------- | | v9 | 6.\* | >16.8 | Hooks, React 18.2 in strict mode | -| v7,v8 | 6.\* | >16.8 < 18.2 | Hooks | +| v7 | 6.\* | >16.8 < 18.2 | Hooks | | v6 | 4.\* / 5.\* | >16.8 <18 | Hooks | | v5 | 4.\* / 5.\* | >0.13 <18 | No, but it is possible to use `` sections inside hook based components | diff --git a/packages/mobx-react/package.json b/packages/mobx-react/package.json index a3edb044d..baf313bf0 100644 --- a/packages/mobx-react/package.json +++ b/packages/mobx-react/package.json @@ -71,6 +71,6 @@ "test:size": "yarn import-size --report . observer", "test:types": "tsc --noEmit", "test:check": "yarn test:types", - "prepublish": "yarn build --target publish" + "prepublishOnly": "yarn build --target publish" } } diff --git a/packages/mobx/package.json b/packages/mobx/package.json index 1caa6dec4..2716a7c90 100644 --- a/packages/mobx/package.json +++ b/packages/mobx/package.json @@ -75,6 +75,6 @@ "test:coverage": "yarn test -i --coverage", "test:size": "yarn import-size --report . observable computed autorun action", "test:check": "yarn test:types", - "prepublish": "node ./scripts/prepublish.js && yarn build --target publish" + "prepublishOnly": "node ./scripts/prepublish.js && yarn build --target publish" } } From 9f80ed7f0023b4d65a7bae243e539f0fb5a992d8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Jul 2023 19:31:28 +0100 Subject: [PATCH 09/15] Version release (#3720) Co-authored-by: github-actions[bot] --- .changeset/fifty-rivers-doubt.md | 5 ----- packages/mobx/CHANGELOG.md | 6 ++++++ packages/mobx/package.json | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 .changeset/fifty-rivers-doubt.md diff --git a/.changeset/fifty-rivers-doubt.md b/.changeset/fifty-rivers-doubt.md deleted file mode 100644 index 19562ef95..000000000 --- a/.changeset/fifty-rivers-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"mobx": patch ---- - -Make trace() noop in production build diff --git a/packages/mobx/CHANGELOG.md b/packages/mobx/CHANGELOG.md index 28b27818b..75955d74f 100644 --- a/packages/mobx/CHANGELOG.md +++ b/packages/mobx/CHANGELOG.md @@ -1,5 +1,11 @@ # mobx +## 6.9.1 + +### Patch Changes + +- [`4792303e`](https://github.com/mobxjs/mobx/commit/4792303ec9119c1ba54134fff7e845d21a1d9337) [#3709](https://github.com/mobxjs/mobx/pull/3709) Thanks [@kubk](https://github.com/kubk)! - Make trace() noop in production build + ## 6.9.0 ### Minor Changes diff --git a/packages/mobx/package.json b/packages/mobx/package.json index 2716a7c90..9146da072 100644 --- a/packages/mobx/package.json +++ b/packages/mobx/package.json @@ -1,6 +1,6 @@ { "name": "mobx", - "version": "6.9.0", + "version": "6.9.1", "description": "Simple, scalable state management.", "source": "src/mobx.ts", "main": "dist/index.js", From 55f78ddc20e84f38a7aa88b99a51ad994e558241 Mon Sep 17 00:00:00 2001 From: Liu Can <313720186@qq.com> Date: Fri, 14 Jul 2023 18:39:48 +0800 Subject: [PATCH 10/15] fix(type): remove makeObservable and makeAutoObservable function proxy option (#3717) --- .changeset/big-ducks-jog.md | 5 +++++ docs/observable-state.md | 2 +- packages/mobx/src/api/makeObservable.ts | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/big-ducks-jog.md diff --git a/.changeset/big-ducks-jog.md b/.changeset/big-ducks-jog.md new file mode 100644 index 000000000..b5a87399e --- /dev/null +++ b/.changeset/big-ducks-jog.md @@ -0,0 +1,5 @@ +--- +"mobx": patch +--- + +remove proxy option for makeObservable and makeAutoObservable diff --git a/docs/observable-state.md b/docs/observable-state.md index 3266d6e68..c9b552a09 100644 --- a/docs/observable-state.md +++ b/docs/observable-state.md @@ -260,7 +260,7 @@ The above APIs take an optional `options` argument which is an object that suppo - **`autoBind: true`** uses `action.bound`/`flow.bound` by default, rather than `action`/`flow`. Does not affect explicitely annotated members. - **`deep: false`** uses `observable.ref` by default, rather than `observable`. Does not affect explicitely annotated members. - **`name: `** gives the object a debug name that is printed in error messages and reflection APIs. -- **`proxy: false`** forces `observable(thing)` to use non-[**proxy**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) implementation. This is a good option if the shape of the object will not change over time, as non-proxied objects are easier to debug and faster. See [avoiding proxies](#avoid-proxies). +- **`proxy: false`** forces `observable(thing)` to use non-[**proxy**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) implementation. This is a good option if the shape of the object will not change over time, as non-proxied objects are easier to debug and faster. This option is **not** available for `make(Auto)Observable`, see [avoiding proxies](#avoid-proxies).
**Note:** options are *sticky* and can be provided only once `options` argument can be provided only for `target` that is NOT observable yet.
diff --git a/packages/mobx/src/api/makeObservable.ts b/packages/mobx/src/api/makeObservable.ts index afdd49d7c..ffb02e85e 100644 --- a/packages/mobx/src/api/makeObservable.ts +++ b/packages/mobx/src/api/makeObservable.ts @@ -23,10 +23,12 @@ import { // Fixes: https://github.com/mobxjs/mobx/issues/2325#issuecomment-691070022 type NoInfer = [T][T extends any ? 0 : never] +type MakeObservableOptions = Omit + export function makeObservable( target: T, annotations?: AnnotationsMap>, - options?: CreateObservableOptions + options?: MakeObservableOptions ): T { const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx] startBatch() @@ -53,7 +55,7 @@ const keysSymbol = Symbol("mobx-keys") export function makeAutoObservable( target: T, overrides?: AnnotationsMap>, - options?: CreateObservableOptions + options?: MakeObservableOptions ): T { if (__DEV__) { if (!isPlainObject(target) && !isPlainObject(Object.getPrototypeOf(target))) { From 1a693efb61e880a583393e00b4d6878ed06bb13e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jul 2023 07:23:58 +0300 Subject: [PATCH 11/15] chore(deps): bump coverallsapp/github-action from 2.2.0 to 2.2.1 (#3723) Bumps [coverallsapp/github-action](https://github.com/coverallsapp/github-action) from 2.2.0 to 2.2.1. - [Release notes](https://github.com/coverallsapp/github-action/releases) - [Commits](https://github.com/coverallsapp/github-action/compare/v2.2.0...v2.2.1) --- updated-dependencies: - dependency-name: coverallsapp/github-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/coveralls.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index 796f1bcd7..0d9466cd8 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -28,6 +28,6 @@ jobs: run: yarn coverage - name: Upload to coveralls - uses: coverallsapp/github-action@v2.2.0 + uses: coverallsapp/github-action@v2.2.1 with: github-token: ${{ secrets.GITHUB_TOKEN }} From bebd5f0507a109145f401c78630ed9d59e4a1101 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Tue, 18 Jul 2023 21:25:29 +0300 Subject: [PATCH 12/15] feat(mobx): add support for AbortSignal for `reaction`, `autorun` and sync `when` (#3727) --- .changeset/thirty-tools-confess.md | 5 +++ docs/reactions.md | 5 ++- packages/mobx/__tests__/v5/base/autorun.js | 31 ++++++++++++++ packages/mobx/__tests__/v5/base/reaction.js | 42 +++++++++++++++++++ .../__tests__/v5/base/typescript-tests.ts | 12 ++++++ packages/mobx/src/api/autorun.ts | 16 ++++--- packages/mobx/src/api/when.ts | 11 +---- packages/mobx/src/core/reaction.ts | 15 ++++--- packages/mobx/src/internal.ts | 1 + .../mobx/src/types/generic-abort-signal.ts | 7 ++++ 10 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 .changeset/thirty-tools-confess.md create mode 100644 packages/mobx/src/types/generic-abort-signal.ts diff --git a/.changeset/thirty-tools-confess.md b/.changeset/thirty-tools-confess.md new file mode 100644 index 000000000..44f4fe574 --- /dev/null +++ b/.changeset/thirty-tools-confess.md @@ -0,0 +1,5 @@ +--- +"mobx": minor +--- + +Added support for `signal` (AbortSignal) in `autorun`, `reaction` and sync `when` options to dispose them diff --git a/docs/reactions.md b/docs/reactions.md index c5ef1849f..aaa6d5c7f 100644 --- a/docs/reactions.md +++ b/docs/reactions.md @@ -364,9 +364,10 @@ Number of milliseconds that can be used to throttle the effect function. If zero Set a limited amount of time that `when` will wait for. If the deadline passes, `when` will reject / throw. -### `signal` _(when)_ +### `signal` -An AbortSignal object instance; allows you to abort waiting for the reaction via an AbortController. This will also cause the returned promise to reject with an error "WHEN_ABORTED". This option is ignored when using an effect function, and only applies with the promised based version. +An AbortSignal object instance; can be used as an alternative method for disposal.
+When used with promise version of `when`, the promise rejects with the "WHEN_ABORTED" error. ### `onError` diff --git a/packages/mobx/__tests__/v5/base/autorun.js b/packages/mobx/__tests__/v5/base/autorun.js index c84a9b3c5..6cf422207 100644 --- a/packages/mobx/__tests__/v5/base/autorun.js +++ b/packages/mobx/__tests__/v5/base/autorun.js @@ -37,6 +37,37 @@ test("autorun can be disposed on first run", function () { expect(values).toEqual([1]) }) +test("autorun can be disposed using AbortSignal", function () { + const a = mobx.observable.box(1) + const ac = new AbortController() + const values = [] + + mobx.autorun(() => { + values.push(a.get()) + }, { signal: ac.signal }) + + a.set(2) + a.set(3) + ac.abort() + a.set(4) + + expect(values).toEqual([1, 2, 3]) +}) + +test("autorun should not run first time when passing already aborted AbortSignal", function () { + const a = mobx.observable.box(1) + const ac = new AbortController() + const values = [] + + ac.abort() + + mobx.autorun(() => { + values.push(a.get()) + }, { signal: ac.signal }) + + expect(values).toEqual([]) +}) + test("autorun warns when passed an action", function () { const action = mobx.action(() => {}) expect.assertions(1) diff --git a/packages/mobx/__tests__/v5/base/reaction.js b/packages/mobx/__tests__/v5/base/reaction.js index ebb264826..4ac9e273f 100644 --- a/packages/mobx/__tests__/v5/base/reaction.js +++ b/packages/mobx/__tests__/v5/base/reaction.js @@ -260,6 +260,48 @@ test("can dispose reaction on first run", () => { expect(valuesEffect).toEqual([]) }) +test("can dispose reaction with AbortSignal", () => { + const a = mobx.observable.box(1) + const ac = new AbortController() + const values = [] + + reaction( + () => a.get(), + (newValue, oldValue) => { + values.push([newValue, oldValue]) + }, + { signal: ac.signal } + ) + + a.set(2) + a.set(3) + ac.abort() + a.set(4) + + expect(values).toEqual([ + [2, 1], + [3, 2] + ]) +}) + +test("fireImmediately should not be honored when passed already aborted AbortSignal", () => { + const a = mobx.observable.box(1) + const ac = new AbortController() + const values = [] + + ac.abort() + + reaction( + () => a.get(), + (newValue) => { + values.push(newValue) + }, + { signal: ac.signal, fireImmediately: true } + ) + + expect(values).toEqual([]) +}) + test("#278 do not rerun if expr output doesn't change", () => { const a = mobx.observable.box(1) const values = [] diff --git a/packages/mobx/__tests__/v5/base/typescript-tests.ts b/packages/mobx/__tests__/v5/base/typescript-tests.ts index 1ec5cf6c3..c0a344615 100644 --- a/packages/mobx/__tests__/v5/base/typescript-tests.ts +++ b/packages/mobx/__tests__/v5/base/typescript-tests.ts @@ -1820,6 +1820,18 @@ test("promised when can be aborted", async () => { } }) +test("sync when can be aborted", async () => { + const x = mobx.observable.box(1) + + const ac = new AbortController() + mobx.when(() => x.get() === 3, () => { + fail("should abort") + }, { signal: ac.signal }) + ac.abort() + + x.set(3); +}) + test("it should support asyncAction as decorator (ts)", async () => { mobx.configure({ enforceActions: "observed" }) diff --git a/packages/mobx/src/api/autorun.ts b/packages/mobx/src/api/autorun.ts index 1ddc23e4b..ddf96ed3f 100644 --- a/packages/mobx/src/api/autorun.ts +++ b/packages/mobx/src/api/autorun.ts @@ -12,7 +12,8 @@ import { isFunction, isPlainObject, die, - allowStateChanges + allowStateChanges, + GenericAbortSignal } from "../internal" export interface IAutorunOptions { @@ -25,6 +26,7 @@ export interface IAutorunOptions { requiresObservable?: boolean scheduler?: (callback: () => void) => any onError?: (error: any) => void + signal?: GenericAbortSignal } /** @@ -88,8 +90,10 @@ export function autorun( view(reaction) } - reaction.schedule_() - return reaction.getDisposer_() + if(!opts?.signal?.aborted) { + reaction.schedule_() + } + return reaction.getDisposer_(opts?.signal) } export type IReactionOptions = IAutorunOptions & { @@ -178,8 +182,10 @@ export function reaction( firstTime = false } - r.schedule_() - return r.getDisposer_() + if(!opts?.signal?.aborted) { + r.schedule_() + } + return r.getDisposer_(opts?.signal) } function wrapErrorHandler(errorHandler, baseFn) { diff --git a/packages/mobx/src/api/when.ts b/packages/mobx/src/api/when.ts index 628a33677..6417aebb9 100644 --- a/packages/mobx/src/api/when.ts +++ b/packages/mobx/src/api/when.ts @@ -6,17 +6,10 @@ import { createAction, getNextId, die, - allowStateChanges + allowStateChanges, + GenericAbortSignal } from "../internal" -// https://github.com/mobxjs/mobx/issues/3582 -interface GenericAbortSignal { - readonly aborted: boolean - onabort?: ((...args: any) => any) | null - addEventListener?: (...args: any) => any - removeEventListener?: (...args: any) => any -} - export interface IWhenOptions { name?: string timeout?: number diff --git a/packages/mobx/src/core/reaction.ts b/packages/mobx/src/core/reaction.ts index dadfdcb13..7baf5bb41 100644 --- a/packages/mobx/src/core/reaction.ts +++ b/packages/mobx/src/core/reaction.ts @@ -18,7 +18,7 @@ import { spyReportStart, startBatch, trace, - trackDerivedFunction + trackDerivedFunction, GenericAbortSignal } from "../internal" /** @@ -195,10 +195,15 @@ export class Reaction implements IDerivation, IReactionPublic { } } - getDisposer_(): IReactionDisposer { - const r = this.dispose.bind(this) as IReactionDisposer - r[$mobx] = this - return r + getDisposer_(abortSignal?: GenericAbortSignal): IReactionDisposer { + const dispose = (() => { + this.dispose() + abortSignal?.removeEventListener?.("abort", dispose) + }) as IReactionDisposer + abortSignal?.addEventListener?.("abort", dispose) + dispose[$mobx] = this + + return dispose } toString() { diff --git a/packages/mobx/src/internal.ts b/packages/mobx/src/internal.ts index c476084fd..1317b234c 100644 --- a/packages/mobx/src/internal.ts +++ b/packages/mobx/src/internal.ts @@ -18,6 +18,7 @@ export * from "./types/flowannotation" export * from "./types/computedannotation" export * from "./types/observableannotation" export * from "./types/autoannotation" +export * from "./types/generic-abort-signal" export * from "./api/observable" export * from "./api/computed" export * from "./core/action" diff --git a/packages/mobx/src/types/generic-abort-signal.ts b/packages/mobx/src/types/generic-abort-signal.ts new file mode 100644 index 000000000..3620496ff --- /dev/null +++ b/packages/mobx/src/types/generic-abort-signal.ts @@ -0,0 +1,7 @@ +// https://github.com/mobxjs/mobx/issues/3582 +export interface GenericAbortSignal { + readonly aborted: boolean + onabort?: ((...args: any) => any) | null + addEventListener?: (...args: any) => any + removeEventListener?: (...args: any) => any +} From ee09a6742bcd2ac7e599b1077b54670c6973d8e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 20:39:56 +0200 Subject: [PATCH 13/15] Version release (#3722) --- .changeset/big-ducks-jog.md | 5 ----- .changeset/thirty-tools-confess.md | 5 ----- packages/mobx/CHANGELOG.md | 10 ++++++++++ packages/mobx/package.json | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 .changeset/big-ducks-jog.md delete mode 100644 .changeset/thirty-tools-confess.md diff --git a/.changeset/big-ducks-jog.md b/.changeset/big-ducks-jog.md deleted file mode 100644 index b5a87399e..000000000 --- a/.changeset/big-ducks-jog.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"mobx": patch ---- - -remove proxy option for makeObservable and makeAutoObservable diff --git a/.changeset/thirty-tools-confess.md b/.changeset/thirty-tools-confess.md deleted file mode 100644 index 44f4fe574..000000000 --- a/.changeset/thirty-tools-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"mobx": minor ---- - -Added support for `signal` (AbortSignal) in `autorun`, `reaction` and sync `when` options to dispose them diff --git a/packages/mobx/CHANGELOG.md b/packages/mobx/CHANGELOG.md index 75955d74f..f12d22aa5 100644 --- a/packages/mobx/CHANGELOG.md +++ b/packages/mobx/CHANGELOG.md @@ -1,5 +1,15 @@ # mobx +## 6.10.0 + +### Minor Changes + +- [`bebd5f05`](https://github.com/mobxjs/mobx/commit/bebd5f0507a109145f401c78630ed9d59e4a1101) [#3727](https://github.com/mobxjs/mobx/pull/3727) Thanks [@rluvaton](https://github.com/rluvaton)! - Added support for `signal` (AbortSignal) in `autorun`, `reaction` and sync `when` options to dispose them + +### Patch Changes + +- [`55f78ddc`](https://github.com/mobxjs/mobx/commit/55f78ddc20e84f38a7aa88b99a51ad994e558241) [#3717](https://github.com/mobxjs/mobx/pull/3717) Thanks [@liucan233](https://github.com/liucan233)! - remove proxy option for makeObservable and makeAutoObservable + ## 6.9.1 ### Patch Changes diff --git a/packages/mobx/package.json b/packages/mobx/package.json index 9146da072..2ae388f86 100644 --- a/packages/mobx/package.json +++ b/packages/mobx/package.json @@ -1,6 +1,6 @@ { "name": "mobx", - "version": "6.9.1", + "version": "6.10.0", "description": "Simple, scalable state management.", "source": "src/mobx.ts", "main": "dist/index.js", From f4129ed091945ddc58ff688fe6baf6f4bb92ea1e Mon Sep 17 00:00:00 2001 From: situ2001 Date: Tue, 25 Jul 2023 03:02:23 +0800 Subject: [PATCH 14/15] chore: add missing returning type and fix typo (#3735) * fix: typo * fix: add missing return type --- packages/mobx-react-lite/src/useObserver.ts | 2 +- packages/mobx/src/core/atom.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mobx-react-lite/src/useObserver.ts b/packages/mobx-react-lite/src/useObserver.ts index 8685d77cf..77ad6241e 100644 --- a/packages/mobx-react-lite/src/useObserver.ts +++ b/packages/mobx-react-lite/src/useObserver.ts @@ -35,7 +35,7 @@ function createReaction(adm: ObserverAdministration) { // BC adm.stateVersion = Symbol() } - // onStoreChange won't be avaliable until the component "mounts". + // onStoreChange won't be available until the component "mounts". // If state changes in between initial render and mount, // `useSyncExternalStore` should handle that by checking the state version and issuing update. adm.onStoreChange?.() diff --git a/packages/mobx/src/core/atom.ts b/packages/mobx/src/core/atom.ts index beff74426..e05f5baf4 100644 --- a/packages/mobx/src/core/atom.ts +++ b/packages/mobx/src/core/atom.ts @@ -19,7 +19,7 @@ export const $mobx = Symbol("mobx administration") export interface IAtom extends IObservable { reportObserved(): boolean - reportChanged() + reportChanged(): void } export class Atom implements IAtom { From d813746cfaa18d80daddee3724562fed6b307c0a Mon Sep 17 00:00:00 2001 From: urugator <11457665+urugator@users.noreply.github.com> Date: Mon, 24 Jul 2023 21:06:00 +0200 Subject: [PATCH 15/15] fix #3730: class component does not react to state changes performed before mount (#3731) * fix * typo * clear flag on unmount, fix typo --- .changeset/polite-laws-provide.md | 5 ++++ .../mobx-react/__tests__/observer.test.tsx | 30 +++++++++++++++++++ packages/mobx-react/src/observerClass.ts | 14 +++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 .changeset/polite-laws-provide.md diff --git a/.changeset/polite-laws-provide.md b/.changeset/polite-laws-provide.md new file mode 100644 index 000000000..d18360b37 --- /dev/null +++ b/.changeset/polite-laws-provide.md @@ -0,0 +1,5 @@ +--- +"mobx-react": patch +--- + +fix #3730: class component does not react to state changes performed before mount diff --git a/packages/mobx-react/__tests__/observer.test.tsx b/packages/mobx-react/__tests__/observer.test.tsx index 864085813..cf8d296f8 100644 --- a/packages/mobx-react/__tests__/observer.test.tsx +++ b/packages/mobx-react/__tests__/observer.test.tsx @@ -1292,3 +1292,33 @@ test(`Observable props/state/context workaround`, () => { expect(reactionResults).toEqual(["000", "100", "110", "111"]) unmount() }) + +test("Class observer can react to changes made before mount #3730", () => { + const o = observable.box(0) + + @observer + class Child extends React.Component { + componentDidMount(): void { + o.set(1) + } + render() { + return "" + } + } + + @observer + class Parent extends React.Component { + render() { + return ( + + {o.get()} + + + ) + } + } + + const { container, unmount } = render() + expect(container).toHaveTextContent("1") + unmount() +}) diff --git a/packages/mobx-react/src/observerClass.ts b/packages/mobx-react/src/observerClass.ts index d4fb346ce..a6b0b4a97 100644 --- a/packages/mobx-react/src/observerClass.ts +++ b/packages/mobx-react/src/observerClass.ts @@ -28,6 +28,7 @@ type ObserverAdministration = { reaction: Reaction | null // also serves as disposed flag forceUpdate: Function | null mounted: boolean // we could use forceUpdate as mounted flag + reactionInvalidatedBeforeMount: boolean name: string // Used only on __DEV__ props: any @@ -42,6 +43,7 @@ function getAdministration(component: Component): ObserverAdministration { return (component[administrationSymbol] ??= { reaction: null, mounted: false, + reactionInvalidatedBeforeMount: false, forceUpdate: null, name: getDisplayName(component.constructor as ComponentClass), state: undefined, @@ -139,12 +141,17 @@ export function makeClassComponentObserver( // As an alternative we could have `admin.instanceRef = new WeakRef(this)`, but lets avoid it if possible. admin.forceUpdate = () => this.forceUpdate() - if (!admin.reaction) { + if (!admin.reaction || admin.reactionInvalidatedBeforeMount) { + // Missing reaction: // 1. Instance was unmounted (reaction disposed) and immediately remounted without running render #3395. // 2. Reaction was disposed by finalization registry before mount. Shouldn't ever happen for class components: // `componentDidMount` runs synchronously after render, but our registry are deferred (can't run in between). // In any case we lost subscriptions to observables, so we have to create new reaction and re-render to resubscribe. // The reaction will be created lazily by following render. + + // Reaction invalidated before mount: + // 1. A descendant's `componenDidMount` invalidated it's parent #3730 + admin.forceUpdate() } return originalComponentDidMount?.apply(this, arguments) @@ -160,6 +167,7 @@ export function makeClassComponentObserver( admin.reaction = null admin.forceUpdate = null admin.mounted = false + admin.reactionInvalidatedBeforeMount = false }) return componentClass @@ -211,7 +219,9 @@ function createReaction(admin: ObserverAdministration) { if (!admin.mounted) { // This is neccessary to avoid react warning about calling forceUpdate on component that isn't mounted yet. // This happens when component is abandoned after render - our reaction is already created and reacts to changes. - // Due to the synchronous nature of `componenDidMount`, we don't have to worry that component could eventually mount and require update. + // `componenDidMount` runs synchronously after `render`, so unlike functional component, there is no delay during which the reaction could be invalidated. + // However `componentDidMount` runs AFTER it's descendants' `componentDidMount`, which CAN invalidate the reaction, see #3730. Therefore remember and forceUpdate on mount. + admin.reactionInvalidatedBeforeMount = true return }