Files
Nitro-V3/src/hooks/inventory/useInventoryUnseenTracker.ts
T
simoleo89 9cc9ef86c0 fix(inventory): stop unseen-tracker mutating shared state arrays in place
resetItems/removeUnseen/the UnseenItemsEvent handler each did `new Map(prevValue)`
(a shallow copy) then spliced/pushed the per-category array returned by
`.get(category)` — the SAME array reference still held by the previous Map. That
mutates state outside React's data flow (breaks under StrictMode double-invoke and
any updater replay). resetItems additionally did `splice(existing.indexOf(id), 1)`
with no guard, so an id not present (indexOf === -1) spliced off the wrong LAST
element. Replace each in-place splice/push with a cloned array set back on the new
Map (filter for removals, spread+push for the merge).
2026-06-13 18:15:19 +02:00

133 lines
4.0 KiB
TypeScript

import { UnseenItemsEvent, UnseenResetCategoryComposer, UnseenResetItemsComposer } from '@nitrots/nitro-renderer';
import { useCallback, useMemo, useState } from 'react';
import { useBetween } from 'use-between';
import { SendMessageComposer } from '../../api';
import { useMessageEvent } from '../events';
const sendResetCategoryMessage = (category: number) => SendMessageComposer(new UnseenResetCategoryComposer(category));
const sendResetItemsMessage = (category: number, itemIds: number[]) => SendMessageComposer(new UnseenResetItemsComposer(category, ...itemIds));
const useInventoryUnseenTrackerState = () =>
{
const [ unseenItems, setUnseenItems ] = useState<Map<number, number[]>>(new Map());
const getCount = useCallback((category: number) => (unseenItems.get(category)?.length || 0), [ unseenItems ]);
const getFullCount = useMemo(() =>
{
let count = 0;
for(const key of unseenItems.keys()) count += getCount(key);
return count;
}, [ unseenItems, getCount ]);
const resetCategory = useCallback((category: number) =>
{
let didReset = true;
setUnseenItems(prevValue =>
{
if(!prevValue.has(category))
{
didReset = false;
return prevValue;
}
const newValue = new Map(prevValue);
newValue.delete(category);
sendResetCategoryMessage(category);
return newValue;
});
return didReset;
}, []);
const resetItems = useCallback((category: number, itemIds: number[]) =>
{
let didReset = true;
setUnseenItems(prevValue =>
{
if(!prevValue.has(category))
{
didReset = false;
return prevValue;
}
const newValue = new Map(prevValue);
const existing = newValue.get(category);
// Replace the per-category array instead of splicing the one still
// referenced by the previous Map, and filter (an absent id used to
// splice(indexOf=-1) and drop the wrong last element).
if(existing) newValue.set(category, existing.filter(id => !itemIds.includes(id)));
sendResetItemsMessage(category, itemIds);
return newValue;
});
return didReset;
}, []);
const isUnseen = useCallback((category: number, itemId: number) =>
{
if(!unseenItems.has(category)) return false;
const items = unseenItems.get(category);
return (items.indexOf(itemId) >= 0);
}, [ unseenItems ]);
const removeUnseen = useCallback((category: number, itemId: number) =>
{
setUnseenItems(prevValue =>
{
if(!prevValue.has(category)) return prevValue;
const newValue = new Map(prevValue);
const items = newValue.get(category);
// Clone the array rather than splicing the one shared with prevValue.
if(items && items.indexOf(itemId) >= 0) newValue.set(category, items.filter(id => id !== itemId));
return newValue;
});
}, []);
useMessageEvent<UnseenItemsEvent>(UnseenItemsEvent, event =>
{
const parser = event.getParser();
setUnseenItems(prevValue =>
{
const newValue = new Map(prevValue);
for(const category of parser.categories)
{
// Clone the existing array so we never push into the one still
// referenced by the previous (shallow-copied) Map.
const merged = [ ...(newValue.get(category) ?? []) ];
const itemIds = parser.getItemsByCategory(category);
for(const itemId of itemIds) if(merged.indexOf(itemId) === -1) merged.push(itemId);
newValue.set(category, merged);
}
return newValue;
});
});
return { getCount, getFullCount, resetCategory, resetItems, isUnseen, removeUnseen };
};
export const useInventoryUnseenTracker = () => useBetween(useInventoryUnseenTrackerState);