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:
parent
b258ac70d6
commit
217617a663
3 changed files with 51 additions and 20 deletions
|
|
@ -22,7 +22,6 @@ export const sRefresher = {
|
|||
navigator: 0,
|
||||
overviewScreen: 0,
|
||||
callNavigator: noop,
|
||||
callHookHarness: noop,
|
||||
callOverviewScreen: noop,
|
||||
callSearchableSettingsList: noop,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,7 +145,6 @@ function refreshSettingsOverviewScreen() {
|
|||
|
||||
function refreshSettingsNavigator() {
|
||||
sRefresher.navigator++
|
||||
sRefresher.callHookHarness()
|
||||
sRefresher.callNavigator()
|
||||
sRefresher.callSearchableSettingsList()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue