UNPKG

4.63 kBPlain TextView Raw
1import { Reaction } from "mobx"
2import React from "react"
3import { printDebugValue } from "./utils/printDebugValue"
4import { isUsingStaticRendering } from "./staticRendering"
5import { observerFinalizationRegistry } from "./utils/observerFinalizationRegistry"
6import { useSyncExternalStore } from "use-sync-external-store/shim"
7
8// Do not store `admRef` (even as part of a closure!) on this object,
9// otherwise it will prevent GC and therefore reaction disposal via FinalizationRegistry.
10type ObserverAdministration = {
11 reaction: Reaction | null // also serves as disposed flag
12 onStoreChange: Function | null // also serves as mounted flag
13 // stateVersion that 'ticks' for every time the reaction fires
14 // tearing is still present,
15 // because there is no cross component synchronization,
16 // but we can use `useSyncExternalStore` API.
17 // TODO: optimize to use number?
18 stateVersion: any
19 name: string
20 // These don't depend on state/props, therefore we can keep them here instead of `useCallback`
21 subscribe: Parameters<typeof React.useSyncExternalStore>[0]
22 getSnapshot: Parameters<typeof React.useSyncExternalStore>[1]
23}
24
25function createReaction(adm: ObserverAdministration) {
26 adm.reaction = new Reaction(`observer${adm.name}`, () => {
27 adm.stateVersion = Symbol()
28 // onStoreChange won't be available until the component "mounts".
29 // If state changes in between initial render and mount,
30 // `useSyncExternalStore` should handle that by checking the state version and issuing update.
31 adm.onStoreChange?.()
32 })
33}
34
35export function useObserver<T>(render: () => T, baseComponentName: string = "observed"): T {
36 if (isUsingStaticRendering()) {
37 return render()
38 }
39
40 const admRef = React.useRef<ObserverAdministration | null>(null)
41
42 if (!admRef.current) {
43 // First render
44 const adm: ObserverAdministration = {
45 reaction: null,
46 onStoreChange: null,
47 stateVersion: Symbol(),
48 name: baseComponentName,
49 subscribe(onStoreChange: () => void) {
50 // Do NOT access admRef here!
51 observerFinalizationRegistry.unregister(adm)
52 adm.onStoreChange = onStoreChange
53 if (!adm.reaction) {
54 // We've lost our reaction and therefore all subscriptions, occurs when:
55 // 1. Timer based finalization registry disposed reaction before component mounted.
56 // 2. React "re-mounts" same component without calling render in between (typically <StrictMode>).
57 // We have to recreate reaction and schedule re-render to recreate subscriptions,
58 // even if state did not change.
59 createReaction(adm)
60 // `onStoreChange` won't force update if subsequent `getSnapshot` returns same value.
61 // So we make sure that is not the case
62 adm.stateVersion = Symbol()
63 }
64
65 return () => {
66 // Do NOT access admRef here!
67 adm.onStoreChange = null
68 adm.reaction?.dispose()
69 adm.reaction = null
70 }
71 },
72 getSnapshot() {
73 // Do NOT access admRef here!
74 return adm.stateVersion
75 }
76 }
77
78 admRef.current = adm
79 }
80
81 const adm = admRef.current!
82
83 if (!adm.reaction) {
84 // First render or reaction was disposed by registry before subscribe
85 createReaction(adm)
86 // StrictMode/ConcurrentMode/Suspense may mean that our component is
87 // rendered and abandoned multiple times, so we need to track leaked
88 // Reactions.
89 observerFinalizationRegistry.register(admRef, adm, adm)
90 }
91
92 React.useDebugValue(adm.reaction!, printDebugValue)
93
94 useSyncExternalStore(
95 // Both of these must be stable, otherwise it would keep resubscribing every render.
96 adm.subscribe,
97 adm.getSnapshot,
98 adm.getSnapshot
99 )
100
101 // render the original component, but have the
102 // reaction track the observables, so that rendering
103 // can be invalidated (see above) once a dependency changes
104 let renderResult!: T
105 let exception
106 adm.reaction!.track(() => {
107 try {
108 renderResult = render()
109 } catch (e) {
110 exception = e
111 }
112 })
113
114 if (exception) {
115 throw exception // re-throw any exceptions caught during rendering
116 }
117
118 return renderResult
119}