UNPKG

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