fix(ui): fix selector memoization

Every `GalleryImage` was rerendering any time the app rerendered bc the selector function itself was not memoized. This resulted in the memoization cache inside the selector constantly being reset.

Same for `BatchImage`.

Also updated memoization for a few other selectors.
This commit is contained in:
psychedelicious 2023-07-04 22:45:45 +10:00
parent c0501ed5c2
commit 1358c5eb7d
8 changed files with 92 additions and 74 deletions

View File

@ -1,28 +1,29 @@
import { Box, Icon, Skeleton } from '@chakra-ui/react'; import { Box, Icon, Skeleton } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FaExclamationCircle } from 'react-icons/fa'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { useGetImageDTOQuery } from 'services/api/endpoints/images'; import IAIDndImage from 'common/components/IAIDndImage';
import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { import {
batchImageRangeEndSelected, batchImageRangeEndSelected,
batchImageSelected, batchImageSelected,
batchImageSelectionToggled, batchImageSelectionToggled,
imageRemovedFromBatch, imageRemovedFromBatch,
} from 'features/batch/store/batchSlice'; } from 'features/batch/store/batchSlice';
import IAIDndImage from 'common/components/IAIDndImage'; import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { createSelector } from '@reduxjs/toolkit'; import { FaExclamationCircle } from 'react-icons/fa';
import { RootState, stateSelector } from 'app/store/store'; import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
const isSelectedSelector = createSelector( const makeSelector = (image_name: string) =>
[stateSelector, (state: RootState, imageName: string) => imageName], createSelector(
(state, imageName) => ({ [stateSelector],
(state) => ({
selection: state.batch.selection, selection: state.batch.selection,
isSelected: state.batch.selection.includes(imageName), isSelected: state.batch.selection.includes(image_name),
}), }),
defaultSelectorOptions defaultSelectorOptions
); );
type BatchImageProps = { type BatchImageProps = {
imageName: string; imageName: string;
@ -37,10 +38,13 @@ const BatchImage = (props: BatchImageProps) => {
} = useGetImageDTOQuery(props.imageName); } = useGetImageDTOQuery(props.imageName);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { isSelected, selection } = useAppSelector((state) => const selector = useMemo(
isSelectedSelector(state, props.imageName) () => makeSelector(props.imageName),
[props.imageName]
); );
const { isSelected, selection } = useAppSelector(selector);
const handleClickRemove = useCallback(() => { const handleClickRemove = useCallback(() => {
dispatch(imageRemovedFromBatch(props.imageName)); dispatch(imageRemovedFromBatch(props.imageName));
}, [dispatch, props.imageName]); }, [dispatch, props.imageName]);

View File

@ -1,25 +1,26 @@
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { FaTrash } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { ImageDTO } from 'services/api/types';
import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd'; import { TypesafeDraggableData } from 'app/components/ImageDnd/typesafeDnd';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import ImageContextMenu from './ImageContextMenu'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice';
import { MouseEvent, memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa';
import { ImageDTO } from 'services/api/types';
import { import {
imageRangeEndSelected, imageRangeEndSelected,
imageSelected, imageSelected,
imageSelectionToggled, imageSelectionToggled,
} from '../store/gallerySlice'; } from '../store/gallerySlice';
import { imageToDeleteSelected } from 'features/imageDeletion/store/imageDeletionSlice'; import ImageContextMenu from './ImageContextMenu';
export const selector = createSelector( export const makeSelector = (image_name: string) =>
[stateSelector, (state, { image_name }: ImageDTO) => image_name], createSelector(
({ gallery }, image_name) => { [stateSelector],
({ gallery }) => {
const isSelected = gallery.selection.includes(image_name); const isSelected = gallery.selection.includes(image_name);
const selection = gallery.selection; const selection = gallery.selection;
return { return {
@ -28,7 +29,7 @@ export const selector = createSelector(
}; };
}, },
defaultSelectorOptions defaultSelectorOptions
); );
interface HoverableImageProps { interface HoverableImageProps {
imageDTO: ImageDTO; imageDTO: ImageDTO;
@ -38,13 +39,13 @@ interface HoverableImageProps {
* Gallery image component with delete/use all/use seed buttons on hover. * Gallery image component with delete/use all/use seed buttons on hover.
*/ */
const GalleryImage = (props: HoverableImageProps) => { const GalleryImage = (props: HoverableImageProps) => {
const { isSelected, selection } = useAppSelector((state) =>
selector(state, props.imageDTO)
);
const { imageDTO } = props; const { imageDTO } = props;
const { image_url, thumbnail_url, image_name } = imageDTO; const { image_url, thumbnail_url, image_name } = imageDTO;
const localSelector = useMemo(() => makeSelector(image_name), [image_name]);
const { isSelected, selection } = useAppSelector(localSelector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,14 +1,19 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { map } from 'lodash-es'; import { map } from 'lodash-es';
import ParamLora from './ParamLora'; import ParamLora from './ParamLora';
const selector = createSelector(stateSelector, ({ lora }) => { const selector = createSelector(
stateSelector,
({ lora }) => {
const { loras } = lora; const { loras } = lora;
return { loras }; return { loras };
}); },
defaultSelectorOptions
);
const ParamLoraList = () => { const ParamLoraList = () => {
const { loras } = useAppSelector(selector); const { loras } = useAppSelector(selector);

View File

@ -1,5 +1,6 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
@ -27,7 +28,8 @@ const selector = createSelector(
shouldUseSliders, shouldUseSliders,
shift, shift,
}; };
} },
defaultSelectorOptions
); );
const ParamCFGScale = () => { const ParamCFGScale = () => {

View File

@ -1,5 +1,6 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setHeight } from 'features/parameters/store/generationSlice'; import { setHeight } from 'features/parameters/store/generationSlice';
@ -25,7 +26,8 @@ const selector = createSelector(
inputMax, inputMax,
step, step,
}; };
} },
defaultSelectorOptions
); );
type ParamHeightProps = Omit< type ParamHeightProps = Omit<

View File

@ -1,17 +1,16 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store'; import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setIterations } from 'features/parameters/store/generationSlice'; import { setIterations } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const selector = createSelector([stateSelector], (state) => { const selector = createSelector(
[stateSelector],
(state) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
state.config.sd.iterations; state.config.sd.iterations;
const { iterations } = state.generation; const { iterations } = state.generation;
@ -31,7 +30,9 @@ const selector = createSelector([stateSelector], (state) => {
shouldUseSliders, shouldUseSliders,
isDisabled, isDisabled,
}; };
}); },
defaultSelectorOptions
);
const ParamIterations = () => { const ParamIterations = () => {
const { const {

View File

@ -1,5 +1,6 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
@ -33,7 +34,8 @@ const selector = createSelector(
step, step,
shouldUseSliders, shouldUseSliders,
}; };
} },
defaultSelectorOptions
); );
const ParamSteps = () => { const ParamSteps = () => {

View File

@ -1,7 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAISlider from 'common/components/IAISlider'; import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { IAIFullSliderProps } from 'common/components/IAISlider'; import IAISlider, { IAIFullSliderProps } from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors'; import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setWidth } from 'features/parameters/store/generationSlice'; import { setWidth } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors'; import { configSelector } from 'features/system/store/configSelectors';
@ -26,7 +26,8 @@ const selector = createSelector(
inputMax, inputMax,
step, step,
}; };
} },
defaultSelectorOptions
); );
type ParamWidthProps = Omit<IAIFullSliderProps, 'label' | 'value' | 'onChange'>; type ParamWidthProps = Omit<IAIFullSliderProps, 'label' | 'value' | 'onChange'>;