Skip to content

Commit

Permalink
Support displayName on anonymous components (#3192)
Browse files Browse the repository at this point in the history
  • Loading branch information
urugator authored Dec 29, 2021
1 parent 488bf76 commit 4887d20
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/warm-foxes-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mobx-react-lite": patch
---

Support customizing `displayName` on anonymous components [#2721](https://github.com/mobxjs/mobx/issues/2721).
40 changes: 39 additions & 1 deletion packages/mobx-react-lite/__tests__/observer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,6 @@ it("should hoist known statics only", () => {
MyHipsterComponent.render = "Nope!"

const wrapped = observer(MyHipsterComponent)
expect(wrapped.displayName).toBe("MyHipsterComponent")
expect(wrapped.randomStaticThing).toEqual(3)
expect(wrapped.defaultProps).toEqual({ x: 3 })
expect(wrapped.propTypes).toEqual({ x: isNumber })
Expand Down Expand Up @@ -996,3 +995,42 @@ it("Throw when trying to set contextType on observer", () => {
/\[mobx-react-lite\] `Component.contextTypes` must be set before applying `observer`./
)
})

test("Anonymous component displayName #3192", () => {
// React prints errors even if we catch em
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {})

// Simulate:
// Error: n_a_m_e(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.
// The point is to get correct displayName in error msg.

let memoError
let observerError

// @ts-ignore
const MemoCmp = React.memo(() => {})
// @ts-ignore
const ObserverCmp = observer(() => {})

ObserverCmp.displayName = MemoCmp.displayName = "n_a_m_e"

try {
render(<MemoCmp />)
} catch (error) {
memoError = error
}

try {
render(<ObserverCmp />)
} catch (error) {
observerError = error
}

expect(memoError).toBeInstanceOf(Error)
expect(observerError).toBeInstanceOf(Error)

expect(memoError.message.includes(MemoCmp.displayName))
expect(MemoCmp.displayName).toEqual(ObserverCmp.displayName)
expect(observerError.message).toEqual(memoError.message)
consoleErrorSpy.mockRestore()
})
39 changes: 25 additions & 14 deletions packages/mobx-react-lite/src/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ export function observer<
options?: Options
): Options extends { forwardRef: true }
? C extends React.RefForwardingComponent<infer TRef, infer P>
? C &
React.MemoExoticComponent<
React.ForwardRefExoticComponent<
React.PropsWithoutRef<P> & React.RefAttributes<TRef>
>
>
: never /* forwardRef set for a non forwarding component */
? C &
React.MemoExoticComponent<
React.ForwardRefExoticComponent<
React.PropsWithoutRef<P> & React.RefAttributes<TRef>
>
>
: never /* forwardRef set for a non forwarding component */
: C & { displayName: string }

// n.b. base case is not used for actual typings or exported in the typing files
Expand All @@ -56,11 +56,16 @@ export function observer<P extends object, TRef = {}>(
const wrappedComponent = (props: P, ref: React.Ref<TRef>) => {
return useObserver(() => baseComponent(props, ref), baseComponentName)
}
wrappedComponent.displayName = baseComponentName

// Support legacy context: `contextTypes` must be applied before `memo`
// Don't set `displayName` for anonymous components,
// so the `displayName` can be customized by user, see #3192.
if (baseComponentName !== "") {
wrappedComponent.displayName = baseComponentName
}

// Support legacy context: `contextTypes` must be applied before `memo`
if ((baseComponent as any).contextTypes) {
wrappedComponent.contextTypes = (baseComponent as any).contextTypes;
wrappedComponent.contextTypes = (baseComponent as any).contextTypes
}

// memo; we are not interested in deep updates
Expand All @@ -78,12 +83,15 @@ export function observer<P extends object, TRef = {}>(
}

copyStaticProperties(baseComponent, memoComponent)
memoComponent.displayName = baseComponentName

if ("production" !== process.env.NODE_ENV) {
Object.defineProperty(memoComponent, 'contextTypes', {
Object.defineProperty(memoComponent, "contextTypes", {
set() {
throw new Error(`[mobx-react-lite] \`${this.displayName || 'Component'}.contextTypes\` must be set before applying \`observer\`.`);
throw new Error(
`[mobx-react-lite] \`${
this.displayName || this.type?.displayName || "Component"
}.contextTypes\` must be set before applying \`observer\`.`
)
}
})
}
Expand All @@ -96,7 +104,10 @@ const hoistBlackList: any = {
$$typeof: true,
render: true,
compare: true,
type: true
type: true,
// Don't redefine `displayName`,
// it's defined as getter-setter pair on `memo` (see #3192).
displayName: true
}

function copyStaticProperties(base: any, target: any) {
Expand Down
2 changes: 1 addition & 1 deletion packages/mobx-react/__tests__/observer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ test("correctly wraps display name of child component", () => {
})

expect(A.name).toEqual("ObserverClass")
expect(B.displayName).toEqual("StatelessObserver")
expect((B as any).type.displayName).toEqual("StatelessObserver")
})

describe("124 - react to changes in this.props via computed", () => {
Expand Down
20 changes: 10 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -12804,7 +12804,16 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"

"string-width@^1.0.2 || 2 || 3 || 4", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
Expand All @@ -12821,15 +12830,6 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0"

string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

string.prototype.matchall@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa"
Expand Down

0 comments on commit 4887d20

Please sign in to comment.