Compare commits

...

2 commits

Author SHA1 Message Date
14cf38afa8
Fix Turnstile ref write during render and the gtag lint error
Turnstile refreshed its callback ref in the render body; move it to an
effect. The gtag shim legitimately pushes the live arguments object, so
scope a disable to that line with a reason. Lint is now error-clean.
2026-07-10 21:49:55 +00:00
9b21adb9cc
Derive prop-synced state during render, not in effects
The nick field, channel +k/+l inputs and avatar hook re-synced from
props by calling setState inside an effect, which triggers an extra
render pass each time. Track the previous prop and adjust during
render instead — one pass, and the avatar now resets on account switch.
2026-07-10 21:48:08 +00:00
5 changed files with 17 additions and 9 deletions

View file

@ -38,7 +38,7 @@ export function Turnstile({ sitekey, onVerify, onError, theme = 'auto' }: {
const id = useRef<string | null>(null);
// keep latest callbacks without re-rendering the widget
const cb = useRef({ onVerify, onError });
cb.current = { onVerify, onError };
useEffect(() => { cb.current = { onVerify, onError }; });
useEffect(() => {
let dead = false;

View file

@ -123,9 +123,12 @@ export function ChanAdminModal() {
const [limitVal, setLimitVal] = useState(curLimit);
useEffect(() => { if (chan) loadBanList(chan); }, [chan, loadBanList]);
// Re-sync the param inputs when the live modes change under us (someone else sets +k/+l).
useEffect(() => { setKeyVal(curKey); }, [curKey]);
useEffect(() => { setLimitVal(curLimit); }, [curLimit]);
// Re-sync the param inputs when the live modes change under us (someone else sets
// +k/+l) — derive during render, no effect setState.
const [prevKey, setPrevKey] = useState(curKey);
const [prevLimit, setPrevLimit] = useState(curLimit);
if (curKey !== prevKey) { setPrevKey(curKey); setKeyVal(curKey); }
if (curLimit !== prevLimit) { setPrevLimit(curLimit); setLimitVal(curLimit); }
if (!buffer || !buffer.isChannel) return null;
const modes = buffer.modes || '';

View file

@ -14,7 +14,8 @@ function ChangeNickField({ hint }: { hint: string }) {
const nick = useActiveChat((s) => s.nick);
const [newNick, setNewNick] = useState(nick);
// Keep the field in sync if the server confirms a nick change elsewhere.
useEffect(() => { setNewNick(nick); }, [nick]);
const [prevNick, setPrevNick] = useState(nick);
if (nick !== prevNick) { setPrevNick(nick); setNewNick(nick); }
function applyNick() {
const n = newNick.trim();

View file

@ -34,6 +34,7 @@ export function initGa(): void {
loaded = true;
const w = window as unknown as { dataLayer: unknown[]; gtag: (...a: unknown[]) => void };
w.dataLayer = w.dataLayer || [];
// eslint-disable-next-line prefer-rest-params -- the canonical gtag shim pushes the live `arguments` object; gtag.js relies on that exact shape
w.gtag = function gtag() { w.dataLayer.push(arguments); };
w.gtag('consent', 'default', { ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', analytics_storage: 'denied', wait_for_update: 500 });
if (getConsent() === 'granted') setGaConsent(true);

View file

@ -54,11 +54,14 @@ function fetchAvatar(account: string): Promise<string | null> {
// React hook: returns the avatar URL for an account (or null while loading / none).
export function useAvatarUrl(account?: string | null): string | null {
const [url, setUrl] = useState<string | null>(
account ? cache.get(account.toLowerCase()) ?? null : null,
);
const seed = () => (account ? cache.get(account.toLowerCase()) ?? null : null);
const [url, setUrl] = useState<string | null>(seed);
// Account switched: reset to its cached value (or null) during render — no
// effect setState, so no cascading re-render.
const [prev, setPrev] = useState(account);
if (account !== prev) { setPrev(account); setUrl(seed()); }
useEffect(() => {
if (!account) { setUrl(null); return; }
if (!account) return;
let dead = false;
fetchAvatar(account).then((u) => { if (!dead) setUrl(u); });
return () => { dead = true; };