Skip to content
This repository has been archived by the owner on Dec 31, 2020. It is now read-only.

Add test for proper reaction disposal in StrictMode #119

Merged
merged 7 commits into from
Apr 3, 2019
20 changes: 14 additions & 6 deletions src/useObserver.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Reaction } from "mobx"
import { useDebugValue, useRef } from "react"
import { useDebugValue, useEffect, useRef } from "react"
import { printDebugValue } from "./printDebugValue"
import { isUsingStaticRendering } from "./staticRendering"
import { useForceUpdate, useUnmount } from "./utils"
import { useForceUpdate } from "./utils"

export type ForceUpdateHook = () => () => void

Expand All @@ -25,17 +25,25 @@ export function useObserver<T>(
const forceUpdate = wantedForceUpdateHook()

const reaction = useRef<Reaction | null>(null)
const committed = useRef(false)

if (!reaction.current) {
// First render for this component. Not yet committed.
reaction.current = new Reaction(`observer(${baseComponentName})`, () => {
forceUpdate()
// Observable has changed. Only force an update if we've definitely
// been committed.
if (committed.current) {
forceUpdate()
}
})
}

useDebugValue(reaction, printDebugValue)

useUnmount(() => {
reaction.current!.dispose()
})
useEffect(() => {
committed.current = true
return () => reaction.current!.dispose()
}, [])

// render the original component, but have the
// reaction track the observables, so that rendering
Expand Down
8 changes: 1 addition & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { useCallback, useEffect, useState } from "react"

const EMPTY_ARRAY: any[] = []

export function useUnmount(fn: () => void) {
useEffect(() => fn, EMPTY_ARRAY)
}
import { useCallback, useState } from "react"

export function useForceUpdate() {
const [, setTick] = useState(0)
Expand Down
42 changes: 42 additions & 0 deletions test/useObserver.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import mockConsole from "jest-mock-console"
import * as mobx from "mobx"
import * as React from "react"
import { act, cleanup, render } from "react-testing-library"

import { useObserver } from "../src"

afterEach(cleanup)

test("uncommitted observing components should not attempt state changes", () => {
const store = mobx.observable({ count: 0 })

const TestComponent = () => useObserver(() => <div>{store.count}</div>)

// Render our observing component wrapped in StrictMode
const rendering = render(
<React.StrictMode>
<TestComponent />
</React.StrictMode>
)

// That will have caused our component to have been rendered
// more than once, but when we unmount it'll only unmount once.
rendering.unmount()

// Trigger a change to the observable. If the reactions were
// not disposed correctly, we'll see some console errors from
// React StrictMode because we're calling state mutators to
// trigger an update.
const restoreConsole = mockConsole()
try {
act(() => {
store.count++
})

// Check to see if any console errors were reported.
// tslint:disable-next-line: no-console
expect(console.error).not.toHaveBeenCalled()
} finally {
restoreConsole()
}
})