Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #3728: observables initialization should not update state version #3732

Merged
merged 10 commits into from
Aug 26, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
8 changes: 8 additions & 0 deletions .changeset/spotty-emus-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"mobx": patch
---

- fix: #3728: Observable initialization updates state version.
- fix: Observable set initialization violates `enforceActions: "always"`.
- fix: Changing keys of observable object does not respect `enforceActions`.
- fix: #3734: `isolateGlobalState: true` makes observer stop to react to store changes
21 changes: 21 additions & 0 deletions packages/mobx-react-lite/__tests__/observer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1107,3 +1107,24 @@ test("StrictMode #3671", async () => {
)
expect(container).toHaveTextContent("1")
})

test("`isolateGlobalState` shouldn't break reactivity #3734", async () => {
mobx.configure({ isolateGlobalState: true })

const o = mobx.observable({ x: 0 })

const Cmp = observer(() => o.x as any)

const { container, unmount } = render(<Cmp />)

expect(container).toHaveTextContent("0")
act(
mobx.action(() => {
o.x++
})
)
expect(container).toHaveTextContent("1")
unmount()

mobx._resetGlobalState()
})
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ 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))
await Promise.resolve()
gc()
await new Promise(resolve => setTimeout(resolve, 0))
// Can take a while (especially on CI) before gc actually calls the registry
await waitFor(
() => {
Expand All @@ -55,7 +54,7 @@ test("uncommitted components should not leak observations", async () => {
},
{
timeout: 10_000,
interval: 200
interval: 150
}
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,19 @@ describe("base useAsObservableSource should work", () => {
const { container } = render(<Parent />)

expect(container.querySelector("span")!.innerHTML).toBe("10")
expect(counterRender).toBe(2)
expect(counterRender).toBe(1)

act(() => {
;(container.querySelector("#inc")! as any).click()
})
expect(container.querySelector("span")!.innerHTML).toBe("11")
expect(counterRender).toBe(3)
expect(counterRender).toBe(2)

act(() => {
;(container.querySelector("#incmultiplier")! as any).click()
})
expect(container.querySelector("span")!.innerHTML).toBe("22")
expect(counterRender).toBe(5) // TODO: should be 3
expect(counterRender).toBe(4) // One from props, second from updating local observable (setState during render)
})
})

Expand Down Expand Up @@ -271,7 +271,12 @@ describe("combining observer with props and stores", () => {
store.x = 10
})

expect(renderedValues).toEqual([10, 10, 15, 15, 20]) // TODO: should have one 15 less
expect(renderedValues).toEqual([
10,
15, // props change
15, // local observable change (setState during render)
20
])

// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,19 @@ describe("base useAsObservableSource should work", () => {
const { container } = render(<Parent />)

expect(container.querySelector("span")!.innerHTML).toBe("10")
expect(counterRender).toBe(2)
expect(counterRender).toBe(1)

act(() => {
;(container.querySelector("#inc")! as any).click()
})
expect(container.querySelector("span")!.innerHTML).toBe("11")
expect(counterRender).toBe(3)
expect(counterRender).toBe(2)

act(() => {
;(container.querySelector("#incmultiplier")! as any).click()
})
expect(container.querySelector("span")!.innerHTML).toBe("22")
expect(counterRender).toBe(5) // TODO: should be 3
expect(counterRender).toBe(4) // One from props, second from updating local observable with effect
})
})

Expand Down Expand Up @@ -337,7 +337,12 @@ describe("combining observer with props and stores", () => {
store.x = 10
})

expect(renderedValues).toEqual([10, 10, 15, 15, 20]) // TODO: should have one 15 less
expect(renderedValues).toEqual([
10,
15, // props change
15, // local observable change during render (localStore.y = props.y)
20
])

expect(container.querySelector("div")!.textContent).toBe("20")
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(4)
expect(renderCount).toBe(3)
})

it("actions can be used", () => {
Expand Down Expand Up @@ -418,19 +418,19 @@ describe("is used to keep observable within component body", () => {
const { container } = render(<Parent />)

expect(container.querySelector("span")!.innerHTML).toBe("10")
expect(counterRender).toBe(2)
expect(counterRender).toBe(1)

act(() => {
;(container.querySelector("#inc")! as any).click()
})
expect(container.querySelector("span")!.innerHTML).toBe("11")
expect(counterRender).toBe(3)
expect(counterRender).toBe(2)

act(() => {
;(container.querySelector("#incmultiplier")! as any).click()
})
expect(container.querySelector("span")!.innerHTML).toBe("22")
expect(counterRender).toBe(5) // TODO: should be 3
expect(counterRender).toBe(4) // One from props, second from updating local observable with effect
})
})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,7 @@ describe("is used to keep observable within component body", () => {
fireEvent.click(div)
expect(div.textContent).toBe("3-2")

// though render 4 times, mobx.observable only called once
expect(renderCount).toBe(4)
expect(renderCount).toBe(3)
})

it("actions can be used", () => {
Expand Down Expand Up @@ -424,19 +423,19 @@ describe("is used to keep observable within component body", () => {
const { container } = render(<Parent />)

expect(container.querySelector("span")!.innerHTML).toBe("10")
expect(counterRender).toBe(2)
expect(counterRender).toBe(1)

act(() => {
;(container.querySelector("#inc")! as any).click()
})
expect(container.querySelector("span")!.innerHTML).toBe("11")
expect(counterRender).toBe(3)
expect(counterRender).toBe(2)

act(() => {
;(container.querySelector("#incmultiplier")! as any).click()
})
expect(container.querySelector("span")!.innerHTML).toBe("22")
expect(counterRender).toBe(5) // TODO: should be 3
expect(counterRender).toBe(4) // One from props, second from updating source (setState during render)
})
})
})
6 changes: 2 additions & 4 deletions packages/mobx-react-lite/src/useObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,8 @@ type ObserverAdministration = {
getSnapshot: Parameters<typeof React.useSyncExternalStore>[1]
}

const mobxGlobalState = _getGlobalState()

// BC
const globalStateVersionIsAvailable = typeof mobxGlobalState.stateVersion !== "undefined"
const globalStateVersionIsAvailable = typeof _getGlobalState().stateVersion !== "undefined"

function createReaction(adm: ObserverAdministration) {
adm.reaction = new Reaction(`observer${adm.name}`, () => {
Expand Down Expand Up @@ -84,7 +82,7 @@ export function useObserver<T>(render: () => T, baseComponentName: string = "obs
getSnapshot() {
// Do NOT access admRef here!
return globalStateVersionIsAvailable
? mobxGlobalState.stateVersion
? _getGlobalState().stateVersion
: adm.stateVersion
}
}
Expand Down
13 changes: 5 additions & 8 deletions packages/mobx-react/__tests__/finalizationRegistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ import { observer } from "../src"

afterEach(cleanup)

function sleep(time: number) {
return new Promise<void>(res => {
setTimeout(res, time)
})
}

// TODO remove once https://github.com/mobxjs/mobx/pull/3620 is merged.
declare class WeakRef<T> {
constructor(object: T)
Expand Down Expand Up @@ -80,9 +74,12 @@ test("should not prevent GC of uncomitted components", async () => {
expect(aConstructorCount).toBe(2)
expect(aMountCount).toBe(1)

await Promise.resolve()
gc()
await sleep(50)
expect(firstARef!.deref()).toBeUndefined()
await waitFor(() => expect(firstARef!.deref()).toBeUndefined(), {
timeout: 10_000,
interval: 150
})

unmount()
})
5 changes: 2 additions & 3 deletions packages/mobx-react/__tests__/hooks.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,9 @@ 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.
2, // props changed
2 // observable source changed (setState during render)
])
expect(container).toHaveTextContent("2")
})
16 changes: 15 additions & 1 deletion packages/mobx/__tests__/v5/base/extendObservable.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
isObservableProp,
isComputedProp,
isAction,
extendObservable
extendObservable,
observable,
reaction
} from "../../../src/mobx"

test("extendObservable should work", function () {
Expand Down Expand Up @@ -160,3 +162,15 @@ test("extendObservable should apply specified decorators", function () {
expect(isAction(box.someFunc)).toBe(true)
expect(box.someFunc()).toEqual(2)
})

test("extendObservable notifies about added keys", () => {
let reactionCalled = false
const o = observable({})
const disposeReaction = reaction(
() => Object.keys(o),
() => (reactionCalled = true)
)
extendObservable(o, { x: 0 })
expect(reactionCalled).toBe(true)
disposeReaction()
})
Loading