fix: use a real Preact hook to force re-renders, not this.forceUpdate()

The previous fix (61f15f0) restored this.forceUpdate() based on
tracing through preact's diff/component internals, which showed
function components are invoked as c.render(...) - a method call
that should bind `this` to a real Component instance with a working
forceUpdate(). That trace was confirmed against the library source,
but you tested it and the settings list still didn't update in place
after adding a command, so something in that chain isn't panning out
in practice (possibly the setTimeout-based render scheduling, possibly
something else) - not worth continuing to debug blind.

Replacing it with the standard, unambiguous approach: a useState
counter bumped to force a re-render, the documented Preact/React
idiom that doesn't depend on any internal `this` plumbing. Applied to
CommandViewer, MacroViewer, and MobileModifyComponent everywhere they
previously called this.forceUpdate().

Please rebuild and re-test: add/remove/reorder/rename/recolor a
command from the settings list without switching tabs, same for
macros, and the mobile icon-picker modal.
This commit is contained in:
johnny1093 2026-07-11 10:17:22 -04:00
parent 61f15f04e0
commit 5da385da2b
3 changed files with 39 additions and 30 deletions

View file

@ -1,5 +1,6 @@
import { Platform } from "obsidian"; import { Platform } from "obsidian";
import { Fragment, h } from "preact"; import { Fragment, h } from "preact";
import { useState } from "preact/hooks";
import t from "src/l10n"; import t from "src/l10n";
import CommanderPlugin from "src/main"; import CommanderPlugin from "src/main";
import { Macro } from "src/types"; import { Macro } from "src/types";
@ -12,10 +13,13 @@ interface MacroBuilderProps {
plugin: CommanderPlugin; plugin: CommanderPlugin;
macros: Macro[]; macros: Macro[];
} }
export default function MacroViewer( export default function MacroViewer({
this: { forceUpdate: () => void }, plugin,
{ plugin, macros }: MacroBuilderProps macros,
): h.JSX.Element { }: MacroBuilderProps): h.JSX.Element {
const [, setUpdateTick] = useState(0);
const forceUpdate = (): void => setUpdateTick((tick) => tick + 1);
const handleBuilder = (macro: Macro, idx?: number): void => { const handleBuilder = (macro: Macro, idx?: number): void => {
const onClose = (updatedMacro: Macro): void => { const onClose = (updatedMacro: Macro): void => {
macros.splice( macros.splice(
@ -25,7 +29,7 @@ export default function MacroViewer(
); );
void plugin.saveSettings(); void plugin.saveSettings();
this.forceUpdate(); forceUpdate();
updateMacroCommands(plugin); updateMacroCommands(plugin);
modal.close(); modal.close();
}; };
@ -36,7 +40,7 @@ export default function MacroViewer(
const handleDelete = (idx: number): void => { const handleDelete = (idx: number): void => {
macros.splice(idx, 1); macros.splice(idx, 1);
void plugin.saveSettings(); void plugin.saveSettings();
this.forceUpdate(); forceUpdate();
updateMacroCommands(plugin); updateMacroCommands(plugin);
}; };

View file

@ -1,4 +1,5 @@
import { createContext, Fragment, h } from "preact"; import { createContext, Fragment, h } from "preact";
import { useState } from "preact/hooks";
import CommanderPlugin from "src/main"; import CommanderPlugin from "src/main";
import CommandComponent from "./commandComponent"; import CommandComponent from "./commandComponent";
import CommandManagerBase from "src/manager/commands/commandManager"; import CommandManagerBase from "src/manager/commands/commandManager";
@ -18,10 +19,15 @@ interface CommandViewerProps {
children?: h.JSX.Element | h.JSX.Element[]; children?: h.JSX.Element | h.JSX.Element[];
sortable?: boolean; sortable?: boolean;
} }
export default function CommandViewer( export default function CommandViewer({
this: { forceUpdate: () => void }, manager,
{ manager, plugin, children, sortable = true }: CommandViewerProps plugin,
): h.JSX.Element { children,
sortable = true,
}: CommandViewerProps): h.JSX.Element {
const [, setUpdateTick] = useState(0);
const forceUpdate = (): void => setUpdateTick((tick) => tick + 1);
return ( return (
<Fragment> <Fragment>
<ManagerContext.Provider value={manager}> <ManagerContext.Provider value={manager}>
@ -45,7 +51,7 @@ export default function CommandViewer(
).didChooseRemove()) ).didChooseRemove())
) { ) {
await manager.removeCommand(cmd); await manager.removeCommand(cmd);
this.forceUpdate(); forceUpdate();
} }
}} }}
handleUp={(): void => { handleUp={(): void => {
@ -55,7 +61,7 @@ export default function CommandViewer(
idx - 1 idx - 1
); );
void manager.reorder(); void manager.reorder();
this.forceUpdate(); forceUpdate();
}} }}
handleDown={(): void => { handleDown={(): void => {
arrayMoveMutable( arrayMoveMutable(
@ -64,7 +70,7 @@ export default function CommandViewer(
idx + 1 idx + 1
); );
void manager.reorder(); void manager.reorder();
this.forceUpdate(); forceUpdate();
}} }}
handleRename={async ( handleRename={async (
name name
@ -72,7 +78,7 @@ export default function CommandViewer(
cmd.name = name; cmd.name = name;
await plugin.saveSettings(); await plugin.saveSettings();
await manager.reorder(); await manager.reorder();
this.forceUpdate(); forceUpdate();
}} }}
handleNewIcon={async (): Promise<void> => { handleNewIcon={async (): Promise<void> => {
const newIcon = const newIcon =
@ -83,7 +89,7 @@ export default function CommandViewer(
cmd.icon = newIcon; cmd.icon = newIcon;
await plugin.saveSettings(); await plugin.saveSettings();
await manager.reorder(); await manager.reorder();
this.forceUpdate(); forceUpdate();
} }
dispatchEvent( dispatchEvent(
new Event("cmdr-icon-changed") new Event("cmdr-icon-changed")
@ -108,7 +114,7 @@ export default function CommandViewer(
mode || modes[currentIdx + 1]; mode || modes[currentIdx + 1];
await plugin.saveSettings(); await plugin.saveSettings();
await manager.reorder(); await manager.reorder();
this.forceUpdate(); forceUpdate();
}} }}
handleColorChange={async ( handleColorChange={async (
color?: string color?: string
@ -116,7 +122,7 @@ export default function CommandViewer(
cmd.color = color; cmd.color = color;
await plugin.saveSettings(); await plugin.saveSettings();
await manager.reorder(); await manager.reorder();
this.forceUpdate(); forceUpdate();
}} }}
/> />
); );
@ -146,7 +152,7 @@ export default function CommandViewer(
const pair = await chooseNewCommand(plugin); const pair = await chooseNewCommand(plugin);
await manager.addCommand(pair); await manager.addCommand(pair);
await manager.reorder(); await manager.reorder();
this.forceUpdate(); forceUpdate();
}} }}
> >
{t("Add command")} {t("Add command")}

View file

@ -1,24 +1,23 @@
import { h } from "preact"; import { h } from "preact";
import { useEffect } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import t from "src/l10n"; import t from "src/l10n";
import { ObsidianIcon } from "src/util"; import { ObsidianIcon } from "src/util";
import MobileModifyModal from "../mobileModifyModal"; import MobileModifyModal from "../mobileModifyModal";
import { ColorPicker } from "./ColorPicker"; import { ColorPicker } from "./ColorPicker";
import CommanderPlugin from "src/main"; import CommanderPlugin from "src/main";
export default function MobileModifyComponent( export default function MobileModifyComponent({
this: { forceUpdate: () => void }, plugin,
{ modal: controller,
plugin, }: {
modal: controller, plugin: CommanderPlugin;
}: { modal: MobileModifyModal;
plugin: CommanderPlugin; }): h.JSX.Element {
modal: MobileModifyModal; const [, setUpdateTick] = useState(0);
}
): h.JSX.Element {
useEffect(() => { useEffect(() => {
const update = (): void => { const update = (): void => {
this.forceUpdate(); setUpdateTick((tick) => tick + 1);
}; };
addEventListener("cmdr-icon-changed", update); addEventListener("cmdr-icon-changed", update);
return (): void => removeEventListener("cmdr-icon-changed", update); return (): void => removeEventListener("cmdr-icon-changed", update);