staff-tags: fix afterRendered crash on memo-wrapped components, bump to 1.0.7
Confirmed live: opening the member list crashed with "TypeError: target is not callable" inside React's renderWithHooks. afterRendered patched element.type directly, which works for plain function components but not for UserRow (React.memo()-wrapped): patching the non-callable memo wrapper object replaces it with a proxy whose *own* stub is callable, which makes `typeof element.type === 'function'` true even though the real value is a memo object - React's reconciler checks that before checking $$typeof, so it takes the plain-function-component path and calls the proxy directly, whose apply trap forwards to Reflect.apply(realMemoWrapper, ...) and throws, since the real wrapper was never callable. Fix: detect a memo-wrapped type and patch its inner .type (the actual render function memo hides) instead of the wrapper itself - genuinely callable, and leaves the wrapper's own shape untouched so React's type detection stays correct. Shared in lib/afterRendered.ts, so it fixes UserRow now and any future memo-wrapped target automatically.
This commit is contained in:
parent
980f1b33eb
commit
d5e2c25e15
2 changed files with 28 additions and 3 deletions
|
|
@ -8,11 +8,36 @@ import type { FC, ReactElement, ReactNode } from 'react'
|
|||
* reference on an object we just created ourselves - nothing else has captured it yet, so there's
|
||||
* no same-chunk-destructuring staleness to worry about. See @revenge-mod/patcher's `after` docs
|
||||
* and the pattern used by Revenge's own `user-badges` plugin (`afterRendered` there).
|
||||
*
|
||||
* For a `React.memo()`-wrapped type, `element.type` is a non-callable `{$$typeof, type, compare}`
|
||||
* object, not a function. Patching `element.type` directly (confirmed live - crashed the member
|
||||
* list on `UserRow`) replaces it with a proxy whose own stub target *is* callable, which fools
|
||||
* React's reconciler into taking the plain-function-component fast path (it checks
|
||||
* `typeof Component === 'function'` before checking `$$typeof`) instead of the memo path - React
|
||||
* then calls the proxy directly, whose `apply` trap forwards to `Reflect.apply(realMemoWrapper,
|
||||
* ...)` and throws `TypeError: target is not callable`, since the real memo wrapper was never
|
||||
* callable. Patching the wrapper's *inner* `.type` instead - the actual render function memo
|
||||
* hides - avoids this: it's genuinely callable, it's what React actually invokes for a memo
|
||||
* component (`updateSimpleMemoComponent` calls the inner function, not the wrapper), and leaving
|
||||
* the wrapper's own shape (`$$typeof`/`compare`) untouched keeps React's own type detection
|
||||
* correct.
|
||||
*/
|
||||
export const afterRendered = (
|
||||
element: ReactElement<any, FC<any>>,
|
||||
hook: (el: ReactNode) => ReactNode,
|
||||
) =>
|
||||
after(element, 'type', el =>
|
||||
) => {
|
||||
const type = element.type as unknown as {
|
||||
type?: unknown
|
||||
$$typeof?: unknown
|
||||
}
|
||||
const isMemoWrapped =
|
||||
typeof element.type !== 'function' && typeof type?.type === 'function'
|
||||
const target = (isMemoWrapped ? type : element) as Record<
|
||||
'type',
|
||||
FC<any>
|
||||
>
|
||||
|
||||
return after(target, 'type', el =>
|
||||
el instanceof Promise ? el.then(hook) : hook(el),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"name": "Staff Tags",
|
||||
"description": "Adds customizable tags (OWNER, ADMIN, STAFF, MOD, and more) next to members based on their server permissions. Supports per-tag colors, gradients, custom text, and visibility toggles.",
|
||||
"author": "Rosie",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"dependencies": {
|
||||
"revenge.api": {
|
||||
"version": ">=1 <2"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue