perf(LiveGroupFilter, ChannelsTable): optimize state updates and re-rendering logic for improved performance

This commit is contained in:
SergeantPanda 2026-05-02 12:42:10 -05:00
parent b470ae68db
commit 7cbbf2a270
2 changed files with 29 additions and 36 deletions

View file

@ -95,8 +95,8 @@ const LiveGroupFilter = ({
? groupStates.find((g) => g.channel_group === configuringGroupId)
: null;
const applyGroupChange = (nextGroupState) => {
setGroupStates(
groupStates.map((state) =>
setGroupStates((prev) =>
prev.map((state) =>
state.channel_group === nextGroupState.channel_group
? nextGroupState
: state
@ -497,8 +497,8 @@ const LiveGroupFilter = ({
}, [playlist, channelGroups]);
const toggleGroupEnabled = (id) => {
setGroupStates(
groupStates.map((state) => ({
setGroupStates((prev) =>
prev.map((state) => ({
...state,
enabled: state.channel_group == id ? !state.enabled : state.enabled,
}))
@ -506,8 +506,8 @@ const LiveGroupFilter = ({
};
const toggleAutoSync = (id) => {
setGroupStates(
groupStates.map((state) => {
setGroupStates((prev) =>
prev.map((state) => {
if (state.channel_group != id) return state;
const turningOn = !state.auto_channel_sync;
const next = { ...state, auto_channel_sync: turningOn };
@ -520,7 +520,7 @@ const LiveGroupFilter = ({
if (currentStart && currentStart > 1) return next;
let proposedStart = 1;
for (const other of groupStates) {
for (const other of prev) {
if (other.channel_group == id) continue;
if (!other.enabled || !other.auto_channel_sync) continue;
const otherMode =
@ -550,8 +550,8 @@ const LiveGroupFilter = ({
// Handle logo selection from LogoForm
const handleLogoSuccess = ({ logo }) => {
if (logo && logo.id && currentEditingGroupId !== null) {
setGroupStates(
groupStates.map((state) => {
setGroupStates((prev) =>
prev.map((state) => {
if (state.channel_group === currentEditingGroupId) {
return {
...state,
@ -564,7 +564,7 @@ const LiveGroupFilter = ({
return state;
})
);
ensureLogosLoaded(); // Refresh logos
ensureLogosLoaded();
}
setLogoModalOpen(false);
setCurrentEditingGroupId(null);
@ -582,8 +582,8 @@ const LiveGroupFilter = ({
};
const selectAll = () => {
setGroupStates(
groupStates.map((state) => ({
setGroupStates((prev) =>
prev.map((state) => ({
...state,
enabled: isVisible(state) ? true : state.enabled,
}))
@ -591,8 +591,8 @@ const LiveGroupFilter = ({
};
const deselectAll = () => {
setGroupStates(
groupStates.map((state) => ({
setGroupStates((prev) =>
prev.map((state) => ({
...state,
enabled: isVisible(state) ? false : state.enabled,
}))
@ -1803,8 +1803,8 @@ const LiveGroupFilter = ({
'fixed'
}
onChange={(value) => {
setGroupStates(
groupStates.map((state) => {
setGroupStates((prev) =>
prev.map((state) => {
if (
state.channel_group === group.channel_group
) {

View file

@ -244,11 +244,10 @@ const ChannelRowActions = React.memo(
</Box>
);
},
// Custom comparator: only re-render when the actual channel changes.
// The row object is a new TanStack Table reference on each render, but
// row.original.id is stable. Callbacks read fresh data at call time.
(prevProps, nextProps) =>
prevProps.row.original.id === nextProps.row.original.id
// Custom comparator: skip re-render when the channel's data object hasn't
// changed. row.original is stable when the underlying channel hasn't been
// updated; it becomes a new reference when the store replaces that channel.
(prevProps, nextProps) => prevProps.row.original === nextProps.row.original
);
const ChannelsTable = ({ onReady }) => {
@ -575,36 +574,30 @@ const ChannelsTable = ({ onReady }) => {
}));
};
const editChannel = async (ch = null, opts = {}) => {
// If forceAdd is set, always open a blank form
const editChannel = useCallback(async (ch = null, opts = {}) => {
if (opts.forceAdd) {
setChannel(null);
setChannelModalOpen(true);
return;
}
// Use table's selected state instead of store state to avoid stale selections
const currentSelection = table ? table.selectedTableIds : [];
console.log('editChannel called with:', {
ch,
currentSelection,
tableExists: !!table,
});
const currentSelection =
useChannelsTableStore.getState().selectedChannelIds;
console.log('editChannel called with:', { ch, currentSelection });
if (currentSelection.length > 1) {
setChannelBatchModalOpen(true);
} else {
// If no channel object is passed but we have a selection, get the selected channel
let channelToEdit = ch;
if (!channelToEdit && currentSelection.length === 1) {
const selectedId = currentSelection[0];
// Use table data since that's what's currently displayed
channelToEdit = data.find((d) => d.id === selectedId);
channelToEdit = useChannelsTableStore
.getState()
.channels.find((d) => d.id === selectedId);
}
setChannel(channelToEdit);
setChannelModalOpen(true);
}
};
}, []);
const deleteChannel = async (id) => {
console.log(`Deleting channel with ID: ${id}`);
@ -1057,7 +1050,7 @@ const ChannelsTable = ({ onReady }) => {
// from the store, so we don't need to recreate columns when logos load.
// Note: tvgsLoaded is intentionally excluded - EditableEPGCell handles loading state internally
// eslint-disable-next-line react-hooks/exhaustive-deps
[selectedProfileId, channelGroups, theme, tvgsById, epgs]
[selectedProfileId, channelGroups, theme, tvgsById, epgs, editChannel]
);
const renderHeaderCell = (header) => {