UNPKG

6.23 kBPlain TextView Raw
1import { forwardRef, memo } from "react"
2
3import { isUsingStaticRendering } from "./staticRendering"
4import { useObserver } from "./useObserver"
5
6let warnObserverOptionsDeprecated = true
7
8const hasSymbol = typeof Symbol === "function" && Symbol.for
9// Using react-is had some issues (and operates on elements, not on types), see #608 / #609
10const ReactForwardRefSymbol = hasSymbol
11 ? Symbol.for("react.forward_ref")
12 : typeof forwardRef === "function" && forwardRef((props: any) => null)["$$typeof"]
13
14const ReactMemoSymbol = hasSymbol
15 ? Symbol.for("react.memo")
16 : typeof memo === "function" && memo((props: any) => null)["$$typeof"]
17
18export interface IObserverOptions {
19 readonly forwardRef?: boolean
20}
21
22export function observer<P extends object, TRef = {}>(
23 baseComponent: React.ForwardRefRenderFunction<TRef, P>,
24 options: IObserverOptions & { forwardRef: true }
25): React.MemoExoticComponent<
26 React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<TRef>>
27>
28
29export function observer<P extends object, TRef = {}>(
30 baseComponent: React.ForwardRefExoticComponent<
31 React.PropsWithoutRef<P> & React.RefAttributes<TRef>
32 >
33): React.MemoExoticComponent<
34 React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<TRef>>
35>
36
37export function observer<P extends object>(
38 baseComponent: React.FunctionComponent<P>,
39 options?: IObserverOptions
40): React.FunctionComponent<P>
41
42export function observer<
43 C extends React.FunctionComponent<any> | React.ForwardRefRenderFunction<any>,
44 Options extends IObserverOptions
45>(
46 baseComponent: C,
47 options?: Options
48): Options extends { forwardRef: true }
49 ? C extends React.ForwardRefRenderFunction<infer TRef, infer P>
50 ? C &
51 React.MemoExoticComponent<
52 React.ForwardRefExoticComponent<
53 React.PropsWithoutRef<P> & React.RefAttributes<TRef>
54 >
55 >
56 : never /* forwardRef set for a non forwarding component */
57 : C & { displayName: string }
58
59// n.b. base case is not used for actual typings or exported in the typing files
60export function observer<P extends object, TRef = {}>(
61 baseComponent:
62 | React.ForwardRefRenderFunction<TRef, P>
63 | React.FunctionComponent<P>
64 | React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<TRef>>,
65 // TODO remove in next major
66 options?: IObserverOptions
67) {
68 if (process.env.NODE_ENV !== "production" && warnObserverOptionsDeprecated && options) {
69 warnObserverOptionsDeprecated = false
70 console.warn(
71 `[mobx-react-lite] \`observer(fn, { forwardRef: true })\` is deprecated, use \`observer(React.forwardRef(fn))\``
72 )
73 }
74
75 if (ReactMemoSymbol && baseComponent["$$typeof"] === ReactMemoSymbol) {
76 throw new Error(
77 `[mobx-react-lite] You are trying to use \`observer\` on a function component wrapped in either another \`observer\` or \`React.memo\`. The observer already applies 'React.memo' for you.`
78 )
79 }
80
81 // The working of observer is explained step by step in this talk: https://www.youtube.com/watch?v=cPF4iBedoF0&feature=youtu.be&t=1307
82 if (isUsingStaticRendering()) {
83 return baseComponent
84 }
85
86 let useForwardRef = options?.forwardRef ?? false
87 let render = baseComponent
88
89 const baseComponentName = baseComponent.displayName || baseComponent.name
90
91 // If already wrapped with forwardRef, unwrap,
92 // so we can patch render and apply memo
93 if (ReactForwardRefSymbol && baseComponent["$$typeof"] === ReactForwardRefSymbol) {
94 useForwardRef = true
95 render = baseComponent["render"]
96 if (typeof render !== "function") {
97 throw new Error(
98 `[mobx-react-lite] \`render\` property of ForwardRef was not a function`
99 )
100 }
101 }
102
103 let observerComponent = (props: any, ref: React.Ref<TRef>) => {
104 return useObserver(() => render(props, ref), baseComponentName)
105 }
106
107 // Don't set `displayName` for anonymous components,
108 // so the `displayName` can be customized by user, see #3192.
109 if (baseComponentName !== "") {
110 ;(observerComponent as React.FunctionComponent).displayName = baseComponentName
111 }
112
113 // Support legacy context: `contextTypes` must be applied before `memo`
114 if ((baseComponent as any).contextTypes) {
115 ;(observerComponent as React.FunctionComponent).contextTypes = (
116 baseComponent as any
117 ).contextTypes
118 }
119
120 if (useForwardRef) {
121 // `forwardRef` must be applied prior `memo`
122 // `forwardRef(observer(cmp))` throws:
123 // "forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))"
124 observerComponent = forwardRef(observerComponent)
125 }
126
127 // memo; we are not interested in deep updates
128 // in props; we assume that if deep objects are changed,
129 // this is in observables, which would have been tracked anyway
130 observerComponent = memo(observerComponent)
131
132 copyStaticProperties(baseComponent, observerComponent)
133
134 if ("production" !== process.env.NODE_ENV) {
135 Object.defineProperty(observerComponent, "contextTypes", {
136 set() {
137 throw new Error(
138 `[mobx-react-lite] \`${
139 this.displayName || this.type?.displayName || "Component"
140 }.contextTypes\` must be set before applying \`observer\`.`
141 )
142 }
143 })
144 }
145
146 return observerComponent
147}
148
149// based on https://github.com/mridgway/hoist-non-react-statics/blob/master/src/index.js
150const hoistBlackList: any = {
151 $$typeof: true,
152 render: true,
153 compare: true,
154 type: true,
155 // Don't redefine `displayName`,
156 // it's defined as getter-setter pair on `memo` (see #3192).
157 displayName: true
158}
159
160function copyStaticProperties(base: any, target: any) {
161 Object.keys(base).forEach(key => {
162 if (!hoistBlackList[key]) {
163 Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(base, key)!)
164 }
165 })
166}