fix(plugin/settings): remount instead of recomputing SettingHookHarness memos

Recomputing causes a React crash because the harness calls additional hooks for every setting.
When we add/remove setting items, hooks are not called the same amount.

React expects the same amount of hooks to be called after each render, so this breaks adding settings items while in Settings.
We can remount it instead to avoid this crash.
This commit is contained in:
PalmDevs 2026-07-31 03:26:00 +07:00
parent b258ac70d6
commit 217617a663
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
3 changed files with 51 additions and 20 deletions

View file

@ -22,7 +22,6 @@ export const sRefresher = {
navigator: 0,
overviewScreen: 0,
callNavigator: noop,
callHookHarness: noop,
callOverviewScreen: noop,
callSearchableSettingsList: noop,
}

View file

@ -145,7 +145,6 @@ function refreshSettingsOverviewScreen() {
function refreshSettingsNavigator() {
sRefresher.navigator++
sRefresher.callHookHarness()
sRefresher.callNavigator()
sRefresher.callSearchableSettingsList()
}

View file

@ -14,10 +14,16 @@ import { React } from '@revenge-mod/react'
import { asap, noop } from '@revenge-mod/utils/callback'
import { getCurrentStack } from '@revenge-mod/utils/error'
import { useReRender } from '@revenge-mod/utils/react'
import { useEffect } from 'react'
import { cloneElement, useEffect } from 'react'
import type { SettingsSection } from '@revenge-mod/discord/modules/settings'
import type { AnyFunction, KeyWithType } from '@revenge-mod/utils/types'
import type { FC, MemoExoticComponent, useMemo } from 'react'
import type {
FC,
MemoExoticComponent,
ReactElement,
ReactNode,
useMemo,
} from 'react'
interface MemoComponentModule {
default: MemoExoticComponent<FC<any>>
@ -41,7 +47,9 @@ type RefreshIdKey = KeyWithType<typeof sRefresher, number>
type RefreshCallbackKey = KeyWithType<typeof sRefresher, () => void>
let DEBUG_patchedNavigator = false
let DEBUG_patchedHookHarness = false
/** @see {remountHookHarness} */
let SettingHookHarness: MemoComponentModule['default'] | undefined
const pluginSettings = registerInternalPlugin(
{
@ -59,12 +67,14 @@ const pluginSettings = registerInternalPlugin(
patchSearchableSettingsList()
if (__DEV__) asap(DEBUG_warnUnpatchedModules)
asap(DEBUG_warnUnpatchedModules)
})
waitForModuleWithImportedPath<MemoComponentModule>(
'modules/settings/native/renderer/SettingHookHarness.tsx',
patchSettingHookHarness,
exports => {
SettingHookHarness = exports.default
},
)
waitForModuleWithImportedPath<MemoComponentModule>(
@ -109,27 +119,47 @@ export default pluginSettings
// #region Patches
function patchSettingHookHarness(exports: MemoComponentModule) {
instead(exports.default, 'type', (args, orig) => {
useRefresherCallback('callHookHarness')
return Reflect.apply(orig, undefined, args)
})
DEBUG_patchedHookHarness = true
}
function patchSettingsNavigator(exports: MemoComponentModule) {
const shouldRefresh = createRefreshTracker('navigator')
// useMemo(() => getSettingScreens(), [])
instead(exports.default, 'type', (args, orig) => {
useRefresherCallback('callNavigator')
return applyWithMemoRefresh(orig, args, shouldRefresh())
const refresh = shouldRefresh()
const el = applyWithMemoRefresh(orig, args, refresh)
return refresh ? remountHookHarness(el) : el
})
DEBUG_patchedNavigator = true
}
function remountHookHarness(el: ReactElement<{ children?: ReactNode[] }>) {
const children = el.props.children
if (Array.isArray(children)) {
const index = children.findIndex(
child =>
(child as ReactElement | undefined)?.type ===
SettingHookHarness,
)
if (index !== -1) {
const newChildren = [...children]
newChildren[index] = cloneElement(children[index] as ReactElement, {
key: `revenge.${sRefresher.navigator}`,
})
return cloneElement(el, { children: newChildren })
}
}
DEBUG_warn('SettingHookHarness was not rendered')
return el
}
function patchSettingsOverviewScreen(exports: SettingsOverviewScreenModule) {
const shouldRefresh = createRefreshTracker('overviewScreen')
@ -288,10 +318,13 @@ function applyWithUseMemoHook(
*/
function DEBUG_warnUnpatchedModules() {
if (!DEBUG_patchedNavigator) DEBUG_warn('SettingsNavigator was not patched')
if (!DEBUG_patchedHookHarness)
DEBUG_warn('SettingHookHarness was not patched')
if (!SettingHookHarness) DEBUG_warn('SettingHookHarness was not found')
}
function DEBUG_warn(message: string) {
nativeLoggingHook(`\u001b[31m${message}\n${getCurrentStack()}\u001b[0m`, 2)
if (__DEV__)
nativeLoggingHook(
`\u001b[31m${message}\n${getCurrentStack()}\u001b[0m`,
2,
)
}