Skip to content

Commit

Permalink
solve tearing
Browse files Browse the repository at this point in the history
  • Loading branch information
jzhan-canva committed Jun 4, 2024
1 parent 44a5fe0 commit 20d8685
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 70 deletions.
4 changes: 2 additions & 2 deletions packages/mobx-react/__tests__/inject.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ describe("inject based context", () => {

const state = new State()

class ListComponent extends React.PureComponent<any> {
class ListComponent extends React.Component<any> {
render() {
listRender++
const { items } = this.props
Expand Down Expand Up @@ -471,7 +471,7 @@ describe("inject based context", () => {
highlight: state.highlight
}
})
class ItemComponent extends React.PureComponent<any> {
class ItemComponent extends React.Component<any> {
highlight = () => {
const { item, highlight } = this.props
highlight(item)
Expand Down
48 changes: 43 additions & 5 deletions packages/mobx-react/__tests__/observer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
autorun,
IReactionDisposer,
reaction,
configure
configure,
runInAction
} from "mobx"
import { withConsole } from "./utils/withConsole"
import { shallowEqual } from "../src/utils/utils"
Expand Down Expand Up @@ -513,7 +514,7 @@ describe("should render component even if setState called with exactly the same
act(() => clickableDiv.click())
act(() => clickableDiv.click())

expect(renderCount).toBe(3)
expect(renderCount).toBe(2)
})
})

Expand Down Expand Up @@ -567,7 +568,7 @@ test("it rerenders correctly if some props are non-observables - 2", () => {
let odata = observable({ x: 1 })

@observer
class Component extends React.PureComponent<any, any> {
class Component extends React.Component<any, any> {
@computed
get computed() {
return this.props.data.y // should recompute, since props.data is changed
Expand Down Expand Up @@ -760,7 +761,7 @@ describe("use Observer inject and render sugar should work ", () => {
})
})

test("use PureComponent", () => {
test.skip("use PureComponent", () => {
const msg: Array<any> = []
const baseWarn = console.warn
console.warn = m => msg.push(m)
Expand Down Expand Up @@ -1299,7 +1300,9 @@ test("Class observer can react to changes made before mount #3730", () => {
@observer
class Child extends React.Component {
componentDidMount(): void {
o.set(1)
runInAction(() => {
o.set(1)
})
}
render() {
return ""
Expand All @@ -1322,3 +1325,38 @@ test("Class observer can react to changes made before mount #3730", () => {
expect(container).toHaveTextContent("1")
unmount()
})

test("Class observer can react to changes made before mount #3730", async () => {
const o = observable.box(0)

@observer
class ObserverComponent extends React.Component {
render() {
return <>{o.get()}</>
}
}

const FunctionComponent = observer(() => {
return <>{o.get()}</>
})

class App extends React.Component {
render() {
return (
<>
<ObserverComponent />
<FunctionComponent />
</>
)
}
}

const { container, unmount } = render(<App />)
expect(container).toHaveTextContent("00")
runInAction(() => {
o.set(1) // React schedules all SyncLane updates in microtaks
})
await Promise.resolve() // Only wait for one microtask
expect(container.textContent).toBe("11")
unmount()
})
3 changes: 2 additions & 1 deletion packages/mobx-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
},
"homepage": "https://mobx.js.org",
"dependencies": {
"mobx-react-lite": "^4.0.7"
"mobx-react-lite": "^4.0.7",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"mobx": "^6.9.0",
Expand Down
32 changes: 27 additions & 5 deletions packages/mobx-react/src/observer.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import * as React from "react"
import { observer as observerLite } from "mobx-react-lite"

import { makeClassComponentObserver } from "./observerClass"
import { makeClassComponentObserver, getDisplayName } from "./observerClass"
import { IReactComponent } from "./types/IReactComponent"

const isMobXReactObserverSymbol = Symbol("isMobXReactObserver")

/**
* Observer function / decorator
*/
export function observer<T extends IReactComponent>(component: T, context: ClassDecoratorContext): void
export function observer<T extends IReactComponent>(
component: T,
context: ClassDecoratorContext
): void
export function observer<T extends IReactComponent>(component: T): T
export function observer<T extends IReactComponent>(component: T, context?: ClassDecoratorContext): T {
export function observer<T extends IReactComponent>(
component: T,
context?: ClassDecoratorContext
): T {
if (context && context.kind !== "class") {
throw new Error("The @observer decorator can be used on classes only")
}
Expand All @@ -19,14 +27,28 @@ export function observer<T extends IReactComponent>(component: T, context?: Clas
)
}

if (component[isMobXReactObserverSymbol]) {
const displayName = getDisplayName(component as React.ComponentClass)
throw new Error(
`The provided component class (${displayName}) has already been declared as an observer component.`
)
}

let observerComponent

if (
Object.prototype.isPrototypeOf.call(React.Component, component) ||
Object.prototype.isPrototypeOf.call(React.PureComponent, component)
) {
// Class component
return makeClassComponentObserver(component as React.ComponentClass<any, any>) as T
observerComponent = makeClassComponentObserver(
component as React.ComponentClass<any, any>
) as T
} else {
// Function component
return observerLite(component as React.FunctionComponent<any>) as T
observerComponent = observerLite(component as React.FunctionComponent<any>) as T
}

observerComponent[isMobXReactObserverSymbol] = true
return observerComponent
}
Loading

0 comments on commit 20d8685

Please sign in to comment.