Keep unchanged metadata stable for memoization

Everytime the metadata got updated, unchanged values could cause rerenders because their reference changed.
This happened e.g., for the "customThemes" which caused the MUI theme to get recreated resulting in all components to rerender due to the theme context change.
This commit is contained in:
schroda
2025-12-13 17:51:58 +01:00
parent bded64b0b1
commit 43d994f843
2 changed files with 96 additions and 11 deletions

View File

@@ -23,6 +23,48 @@ import { applyMetadataMigrations } from '@/features/metadata/services/MetadataMi
import { SourceIdInfo } from '@/features/source/Source.types.ts'; import { SourceIdInfo } from '@/features/source/Source.types.ts';
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts'; import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
import { APP_METADATA } from '@/features/metadata/Metadata.constants.ts'; import { APP_METADATA } from '@/features/metadata/Metadata.constants.ts';
import { MetadataValueCache } from '@/features/metadata/services/MetadataValueCache.ts';
const getHolderId = (
type: MetadataHolderType,
metadataHolder:
| MetadataHolder
| (MangaIdInfo & MetadataHolder)
| (ChapterIdInfo & MetadataHolder)
| (CategoryIdInfo & MetadataHolder)
| (SourceIdInfo & MetadataHolder),
): string | number | undefined => {
switch (type) {
case 'global':
return undefined;
case 'manga':
return (metadataHolder as MangaIdInfo).id;
case 'chapter':
return (metadataHolder as ChapterIdInfo).id;
case 'category':
return (metadataHolder as CategoryIdInfo).id;
case 'source':
return (metadataHolder as SourceIdInfo).id;
default:
return undefined;
}
};
const getRawMetadataValueFrom = (
metadata: Metadata | undefined,
key: string,
prefixes?: string[],
): string | undefined => {
if (
metadata === undefined ||
!doesMetadataKeyExistIn(metadata, key, prefixes) ||
metadata[getMetadataKey(key, prefixes)] === undefined
) {
return undefined;
}
return metadata[getMetadataKey(key, prefixes)];
};
const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>( const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
metadata: Metadata | undefined, metadata: Metadata | undefined,
@@ -30,15 +72,13 @@ const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends Allowed
defaultValue?: Value, defaultValue?: Value,
prefixes?: string[], prefixes?: string[],
): Value | undefined => { ): Value | undefined => {
if ( const rawValue = getRawMetadataValueFrom(metadata, key, prefixes);
metadata === undefined ||
!doesMetadataKeyExistIn(metadata, key, prefixes) || if (rawValue === undefined) {
metadata[getMetadataKey(key, prefixes)] === undefined
) {
return defaultValue; return defaultValue;
} }
const convertedValue = convertValueFromMetadata(key, metadata[getMetadataKey(key, prefixes)], defaultValue); const convertedValue = convertValueFromMetadata(key, rawValue, defaultValue);
return APP_METADATA[key].toConstrainedValue?.(convertedValue) ?? convertedValue; return APP_METADATA[key].toConstrainedValue?.(convertedValue) ?? convertedValue;
}; };
@@ -92,13 +132,18 @@ export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKey
): METADATA { ): METADATA {
const migratedMetadata = applyMetadataMigrations(type, metadataHolder, useEffectFn); const migratedMetadata = applyMetadataMigrations(type, metadataHolder, useEffectFn);
const appMetadata = {} as METADATA; const appMetadata = {} as METADATA;
const holderId = getHolderId(type, metadataHolder);
Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => { Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => {
appMetadata[key as AppMetadataKeys] = getMetadataValueFrom( const rawValue = getRawMetadataValueFrom(migratedMetadata, key as AppMetadataKeys, prefixes);
migratedMetadata, const newValue = getMetadataValueFrom(migratedMetadata, key as AppMetadataKeys, defaultValue, prefixes);
key as AppMetadataKeys,
defaultValue, appMetadata[key as AppMetadataKeys] = MetadataValueCache.getStableValue(
prefixes, type,
holderId,
getMetadataKey(key, prefixes),
rawValue,
newValue,
); );
}); });

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { MetadataHolderType } from '@/features/metadata/Metadata.types.ts';
export class MetadataValueCache {
private static convertedValueByKey = new Map<string, unknown>();
private static rawValueByKey = new Map<string, string | undefined>();
private static getCacheKey(type: MetadataHolderType, holderId: string | number | undefined, key: string): string {
return `${type}::${holderId ?? ''}::${key}`;
}
static getStableValue<T>(
type: MetadataHolderType,
holderId: string | number | undefined,
key: string,
rawValue: string | undefined,
newValue: T,
): T {
const cacheKey = this.getCacheKey(type, holderId, key);
const cachedRawValue = this.rawValueByKey.get(cacheKey);
const cachedConvertedValue = this.convertedValueByKey.get(cacheKey);
if (cachedRawValue !== undefined && rawValue === cachedRawValue) {
return cachedConvertedValue as T;
}
this.rawValueByKey.set(cacheKey, rawValue);
this.convertedValueByKey.set(cacheKey, newValue);
return newValue;
}
}