feat(ui): more memoization

This commit is contained in:
psychedelicious 2023-12-29 16:52:29 +11:00 committed by Kent Keirsey
parent 10fd4f6a61
commit 3ce8f3d6fe
24 changed files with 161 additions and 79 deletions

@ -30,6 +30,8 @@ module.exports = {
'unused-imports',
'simple-import-sort',
'eslint-plugin-import',
// These rules are too strict for normal usage, but are useful for optimizing rerenders
// '@arthurgeron/react-usememo',
],
root: true,
rules: {
@ -60,6 +62,18 @@ module.exports = {
argsIgnorePattern: '^_',
},
],
// These rules are too strict for normal usage, but are useful for optimizing rerenders
// '@arthurgeron/react-usememo/require-usememo': [
// 'warn',
// {
// strict: false,
// checkHookReturnObject: false,
// fix: { addImports: true },
// checkHookCalls: false,
// },
// ],
// '@arthurgeron/react-usememo/require-memo': 'warn',
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-empty-interface': [

@ -120,6 +120,7 @@
"ts-toolbelt": "^9.6.0"
},
"devDependencies": {
"@arthurgeron/eslint-plugin-react-usememo": "^2.2.1",
"@chakra-ui/cli": "^2.4.1",
"@storybook/addon-docs": "^7.6.6",
"@storybook/addon-essentials": "^7.6.6",

@ -184,6 +184,9 @@ dependencies:
version: 2.1.0(zod@3.22.4)
devDependencies:
'@arthurgeron/eslint-plugin-react-usememo':
specifier: ^2.2.1
version: 2.2.1
'@chakra-ui/cli':
specifier: ^2.4.1
version: 2.4.1
@ -339,6 +342,13 @@ packages:
'@jridgewell/trace-mapping': 0.3.20
dev: true
/@arthurgeron/eslint-plugin-react-usememo@2.2.1:
resolution: {integrity: sha512-XMYGogzqQYR7PC4F4ve+loBTrvsE7AHnhiBjUTBMW5hbFecbg6ReTQwj36WsV4vRXNnDRD3vfwkO4fH8WHcOog==}
dependencies:
minimatch: 9.0.3
uuid: 9.0.1
dev: true
/@aw-web-design/x-default-browser@1.4.126:
resolution: {integrity: sha512-Xk1sIhyNC/esHGGVjL/niHLowM0csl/kFO5uawBy4IrWwy0o1G8LGt3jP6nmWGz+USxeeqbihAmp/oVZju6wug==}
hasBin: true

@ -12,7 +12,12 @@
* - increment it in `onPaneClick` (and wherever else we want to close the menu)
* - `useEffect()` to close the menu when `globalContextMenuCloseTrigger` changes
*/
import type { MenuButtonProps, MenuProps, PortalProps } from '@chakra-ui/react';
import type {
ChakraProps,
MenuButtonProps,
MenuProps,
PortalProps,
} from '@chakra-ui/react';
import { Portal, useEventListener } from '@chakra-ui/react';
import { InvMenu, InvMenuButton } from 'common/components/InvMenu/wrapper';
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
@ -102,7 +107,7 @@ export const InvContextMenu = typedMemo(
cursor="default"
bg="transparent"
size="sm"
_hover={{ bg: 'transparent' }}
_hover={_hover}
{...props.menuButtonProps}
/>
{props.renderMenu()}
@ -114,6 +119,8 @@ export const InvContextMenu = typedMemo(
}
);
const _hover: ChakraProps['_hover'] = { bg: 'transparent' };
Object.assign(InvContextMenu, {
displayName: 'InvContextMenu',
});

@ -1,5 +1,5 @@
// https://stackoverflow.com/a/73731908
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
export function useSingleAndDoubleClick(
handleSingleClick: () => void,
@ -23,5 +23,7 @@ export function useSingleAndDoubleClick(
return () => clearTimeout(timer);
}, [click, handleSingleClick, handleDoubleClick, delay]);
return () => setClick((prev) => prev + 1);
const onClick = useCallback(() => setClick((prev) => prev + 1), []);
return onClick;
}

@ -175,6 +175,8 @@ const IAICanvas = () => {
[stageCursor]
);
const scale= useMemo(()=>({x: stageScale, y: stageScale}),[stageScale])
return (
<Flex
id="canvas-container"
@ -193,7 +195,7 @@ const IAICanvas = () => {
y={stageCoordinates.y}
width={stageDimensions.width}
height={stageDimensions.height}
scale={{ x: stageScale, y: stageScale }}
scale={scale}
onTouchStart={handleMouseDown}
onTouchMove={handleMouseMove}
onTouchEnd={handleMouseUp}

@ -5,7 +5,7 @@ import { rgbaColorToString } from 'features/canvas/util/colorToString';
import type Konva from 'konva';
import type { RectConfig } from 'konva/lib/shapes/Rect';
import { isNumber } from 'lodash-es';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Rect } from 'react-konva';
export const canvasMaskCompositerSelector = createMemoizedSelector(
@ -147,6 +147,11 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
return () => clearInterval(timer);
}, []);
const fillPatternScale = useMemo(
() => ({ x: 1 / stageScale, y: 1 / stageScale }),
[stageScale]
);
if (
!fillPatternImage ||
!isNumber(stageCoordinates.x) ||
@ -168,7 +173,7 @@ const IAICanvasMaskCompositer = (props: IAICanvasMaskCompositerProps) => {
fillPatternImage={fillPatternImage}
fillPatternOffsetY={!isNumber(offset) ? 0 : offset}
fillPatternRepeat="repeat"
fillPatternScale={{ x: 1 / stageScale, y: 1 / stageScale }}
fillPatternScale={fillPatternScale}
listening={true}
globalCompositeOperation="source-in"
{...rest}

@ -7,6 +7,8 @@ import { Group, Rect } from 'react-konva';
import IAICanvasImage from './IAICanvasImage';
const dash = [4, 4]
const selector = createMemoizedSelector([stateSelector], ({ canvas }) => {
const {
layerState,
@ -69,7 +71,7 @@ const IAICanvasStagingArea = (props: Props) => {
y={y}
width={width}
height={height}
dash={[4, 4]}
dash={dash}
strokeWidth={1}
stroke="black"
strokeScaleEnabled={false}

@ -21,6 +21,8 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Group, Rect, Transformer } from 'react-konva';
const borderDash = [4, 4];
const boundingBoxPreviewSelector = createMemoizedSelector(
[stateSelector],
({ canvas, generation }) => {
@ -289,11 +291,11 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
anchorFill="rgba(212,216,234,1)"
anchorSize={15}
anchorStroke="rgb(42,42,42)"
borderDash={[4, 4]}
borderDash={borderDash}
borderEnabled={true}
borderStroke="black"
draggable={false}
enabledAnchors={tool === 'move' ? undefined : []}
enabledAnchors={tool === 'move' ? undefined : emptyArray}
flipEnabled={false}
ignoreStroke={true}
keepRatio={false}
@ -311,3 +313,5 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
};
export default memo(IAICanvasBoundingBox);
const emptyArray: string[] = []

@ -261,7 +261,7 @@ const IAICanvasToolChooserOptions = () => {
max={100}
step={1}
onChange={handleChangeBrushSize}
marks={[1, 25, 50, 75, 100]}
marks={marks}
/>
<InvNumberInput
value={brushSize}
@ -288,3 +288,5 @@ const IAICanvasToolChooserOptions = () => {
};
export default memo(IAICanvasToolChooserOptions);
const marks = [1, 25, 50, 75, 100];

@ -8,50 +8,52 @@ import {
getCanvasStage,
} from 'features/canvas/util/konvaInstanceProvider';
import Konva from 'konva';
import { useCallback } from 'react';
const useColorPicker = () => {
const dispatch = useAppDispatch();
const canvasBaseLayer = getCanvasBaseLayer();
const stage = getCanvasStage();
return {
updateColorUnderCursor: () => {
if (!stage || !canvasBaseLayer) {
return;
}
const updateColorUnderCursor = useCallback(() => {
if (!stage || !canvasBaseLayer) {
return;
}
const position = stage.getPointerPosition();
const position = stage.getPointerPosition();
if (!position) {
return;
}
if (!position) {
return;
}
const pixelRatio = Konva.pixelRatio;
const pixelRatio = Konva.pixelRatio;
const [r, g, b, a] = canvasBaseLayer
.getContext()
.getImageData(
position.x * pixelRatio,
position.y * pixelRatio,
1,
1
).data;
const [r, g, b, a] = canvasBaseLayer
.getContext()
.getImageData(
position.x * pixelRatio,
position.y * pixelRatio,
1,
1
).data;
if (
r === undefined ||
g === undefined ||
b === undefined ||
a === undefined
) {
return;
}
if (
r === undefined ||
g === undefined ||
b === undefined ||
a === undefined
) {
return;
}
dispatch(setColorPickerColor({ r, g, b, a }));
},
commitColorUnderCursor: () => {
dispatch(commitColorPickerColor());
},
};
dispatch(setColorPickerColor({ r, g, b, a }));
}, [canvasBaseLayer, dispatch, stage]);
const commitColorUnderCursor = useCallback(() => {
dispatch(commitColorPickerColor());
}, [dispatch]);
return { updateColorUnderCursor, commitColorUnderCursor };
};
export default useColorPicker;

@ -1,3 +1,4 @@
import type { SystemStyleObject } from '@chakra-ui/react';
import { Box, Flex, Spinner } from '@chakra-ui/react';
import { skipToken } from '@reduxjs/toolkit/query';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
@ -220,13 +221,13 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
onClick={handleSaveControlImage}
icon={controlImage ? <FaSave size={16} /> : undefined}
tooltip={t('controlnet.saveControlImage')}
styleOverrides={{ marginTop: 6 }}
styleOverrides={saveControlImageStyleOverrides}
/>
<IAIDndImageIcon
onClick={handleSetControlImageToDimensions}
icon={controlImage ? <FaRulerVertical size={16} /> : undefined}
tooltip={t('controlnet.setControlImageDimensions')}
styleOverrides={{ marginTop: 12 }}
styleOverrides={setControlImageDimensionsStyleOverrides}
/>
</>
@ -251,3 +252,6 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => {
};
export default memo(ControlAdapterImagePreview);
const saveControlImageStyleOverrides: SystemStyleObject = { mt: 6 };
const setControlImageDimensionsStyleOverrides: SystemStyleObject = { mt: 12 };

@ -7,7 +7,7 @@ import {
controlAdapterBeginStepPctChanged,
controlAdapterEndStepPctChanged,
} from 'features/controlAdapters/store/controlAdaptersSlice';
import { memo, useCallback } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
type Props = {
@ -55,6 +55,11 @@ export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => {
);
}, [dispatch, id]);
const value = useMemo<[number, number]>(
() => [stepPcts?.beginStepPct ?? 0, stepPcts?.endStepPct ?? 1],
[stepPcts]
);
if (!stepPcts) {
return null;
}
@ -67,8 +72,8 @@ export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => {
orientation="vertical"
>
<InvRangeSlider
aria-label={['Begin Step %', 'End Step %']}
value={[stepPcts.beginStepPct, stepPcts.endStepPct]}
aria-label={ariaLabel}
value={value}
onChange={onChange}
onReset={onReset}
min={0}
@ -83,3 +88,5 @@ export const ParamControlAdapterBeginEnd = memo(({ id }: Props) => {
});
ParamControlAdapterBeginEnd.displayName = 'ParamControlAdapterBeginEnd';
const ariaLabel = ['Begin Step %', 'End Step %'];

@ -42,10 +42,12 @@ const ParamControlAdapterWeight = ({ id }: ParamControlAdapterWeightProps) => {
min={0}
max={2}
step={0.01}
marks={[0, 1, 2]}
marks={marks}
/>
</InvControl>
);
};
export default memo(ParamControlAdapterWeight);
const marks = [0, 1, 2];

@ -83,7 +83,7 @@ const MlsdImageProcessor = (props: Props) => {
onReset={handleDetectResolutionReset}
min={0}
max={4096}
marks={[0, 4096]}
marks={marks0to4096}
withNumberInput
/>
</InvControl>
@ -97,7 +97,7 @@ const MlsdImageProcessor = (props: Props) => {
onReset={handleImageResolutionReset}
min={0}
max={4096}
marks={[0, 4096]}
marks={marks0to4096}
withNumberInput
/>
</InvControl>
@ -109,7 +109,7 @@ const MlsdImageProcessor = (props: Props) => {
min={0}
max={1}
step={0.01}
marks={[0, 1]}
marks={marks0to1}
withNumberInput
/>
</InvControl>
@ -121,7 +121,7 @@ const MlsdImageProcessor = (props: Props) => {
min={0}
max={1}
step={0.01}
marks={[0, 1]}
marks={marks0to1}
withNumberInput
/>
</InvControl>
@ -130,3 +130,6 @@ const MlsdImageProcessor = (props: Props) => {
};
export default memo(MlsdImageProcessor);
const marks0to4096 = [0, 4096];
const marks0to1 = [0, 1];

@ -19,7 +19,7 @@ import { customPointerWithin } from 'features/dnd/util/customPointerWithin';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import type { CSSProperties, PropsWithChildren } from 'react';
import { memo, useCallback, useState } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { DndContextTypesafe } from './DndContextTypesafe';
import DragPreview from './DragPreview';
@ -78,6 +78,7 @@ const AppDndContext = (props: PropsWithChildren) => {
const sensors = useSensors(mouseSensor, touchSensor);
const scaledModifier = useScaledModifer();
const modifiers = useMemo(() => [scaledModifier], [scaledModifier]);
return (
<DndContextTypesafe
@ -90,7 +91,7 @@ const AppDndContext = (props: PropsWithChildren) => {
{props.children}
<DragOverlay
dropAnimation={null}
modifiers={[scaledModifier]}
modifiers={modifiers}
style={dragOverlayStyles}
>
<AnimatePresence>

@ -1,6 +1,7 @@
import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvControl } from 'common/components/InvControl/InvControl';
import type { InvLabelProps } from 'common/components/InvControl/types';
import { InvSwitch } from 'common/components/InvSwitch/wrapper';
import { setHrfEnabled } from 'features/hrf/store/hrfSlice';
import type { ChangeEvent } from 'react';
@ -22,10 +23,12 @@ export default function ParamHrfToggle() {
return (
<InvControl
label={t('hrf.enableHrf')}
labelProps={{ flexGrow: 1 }}
labelProps={labelProps}
w="full"
>
<InvSwitch isChecked={hrfEnabled} onChange={handleHrfEnabled} />
</InvControl>
);
}
const labelProps: InvLabelProps = { flexGrow: 1 };

@ -62,7 +62,7 @@ export const LoRACard = memo((props: LoRACardProps) => {
max={2}
step={0.01}
onReset={onReset}
marks={[-1, 0, 1, 2]}
marks={marks}
/>
<InvNumberInput
value={lora.weight}
@ -79,3 +79,5 @@ export const LoRACard = memo((props: LoRACardProps) => {
});
LoRACard.displayName = 'LoRACard';
const marks = [-1, 0, 1, 2];

@ -55,10 +55,7 @@ const EditableNodeTitle = ({ nodeId, title }: Props) => {
fontWeight="semibold"
>
<EditablePreview noOfLines={1} />
<EditableInput
className="nodrag"
_focusVisible={{ boxShadow: 'none' }}
/>
<EditableInput className="nodrag" />
</Editable>
</Flex>
);

@ -5,7 +5,7 @@ import { InvControl } from 'common/components/InvControl/InvControl';
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { heightChanged } from 'features/parameters/store/generationSlice';
import { memo, useCallback } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector(
@ -49,6 +49,8 @@ export const ParamHeight = memo(() => {
dispatch(heightChanged(initial));
}, [dispatch, initial]);
const marks = useMemo(()=>[min, initial, max], [min, initial, max])
return (
<InvControl label={t('parameters.height')}>
<InvSlider
@ -59,7 +61,7 @@ export const ParamHeight = memo(() => {
max={max}
step={step}
fineStep={fineStep}
marks={[min, initial, max]}
marks={marks}
/>
<InvNumberInput
value={height}

@ -1,5 +1,6 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
import type { InvNumberInputFieldProps } from 'common/components/InvNumberInput/types';
import { setIterations } from 'features/parameters/store/generationSlice';
import { memo, useCallback } from 'react';
@ -23,14 +24,16 @@ const ParamIterations = () => {
value={iterations}
h="full"
w="216px"
numberInputFieldProps={{
ps: '144px',
borderInlineStartRadius: 'base',
h: 'full',
textAlign: 'center',
}}
numberInputFieldProps={numberInputFieldProps}
/>
);
};
export default memo(ParamIterations);
const numberInputFieldProps: InvNumberInputFieldProps = {
ps: '144px',
borderInlineStartRadius: 'base',
h: 'full',
textAlign: 'center',
};

@ -5,7 +5,7 @@ import { InvControl } from 'common/components/InvControl/InvControl';
import { InvNumberInput } from 'common/components/InvNumberInput/InvNumberInput';
import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { widthChanged } from 'features/parameters/store/generationSlice';
import { memo, useCallback } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createMemoizedSelector(
@ -48,6 +48,8 @@ export const ParamWidth = memo(() => {
dispatch(widthChanged(initial));
}, [dispatch, initial]);
const marks = useMemo(() => [min, initial, max], [min, initial, max]);
return (
<InvControl label={t('parameters.width')}>
<InvSlider
@ -58,7 +60,7 @@ export const ParamWidth = memo(() => {
max={max}
step={step}
fineStep={fineStep}
marks={[min, initial, max]}
marks={marks}
/>
<InvNumberInput
value={width}

@ -1,12 +1,15 @@
import type { RootState } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InvControl } from 'common/components/InvControl/InvControl';
import type { InvLabelProps } from 'common/components/InvControl/types';
import { InvSwitch } from 'common/components/InvSwitch/wrapper';
import { setShouldFitToWidthHeight } from 'features/parameters/store/generationSlice';
import type { ChangeEvent } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const labelProps: InvLabelProps = { flexGrow: 1 };
export default function ImageToImageFit() {
const dispatch = useAppDispatch();
@ -26,7 +29,7 @@ export default function ImageToImageFit() {
return (
<InvControl
label={t('parameters.imageFit')}
labelProps={{ flexGrow: 1 }}
labelProps={labelProps}
w="full"
>
<InvSwitch

@ -1,4 +1,4 @@
import type { ChakraProps } from '@chakra-ui/react';
import type { ChakraProps, CollapseProps } from '@chakra-ui/react';
import { Collapse, Flex } from '@chakra-ui/react';
import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup';
import { InvIconButton } from 'common/components/InvIconButton/InvIconButton';
@ -63,6 +63,7 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
[item.status]
);
const icon = useMemo(() => (<FaTimes />), []);
return (
<Flex
flexDir="column"
@ -137,21 +138,22 @@ const QueueItemComponent = ({ index, item, context }: InnerItemProps) => {
isDisabled={isCanceled}
isLoading={isLoading}
aria-label={t('queue.cancelItem')}
icon={<FaTimes />}
icon={icon}
/>
</InvButtonGroup>
</Flex>
</Flex>
<Collapse
in={isOpen}
transition={{ enter: { duration: 0.1 }, exit: { duration: 0.1 } }}
unmountOnExit={true}
>
<Collapse in={isOpen} transition={transition} unmountOnExit={true}>
<QueueItemDetail queueItemDTO={item} />
</Collapse>
</Flex>
);
};
const transition: CollapseProps['transition'] = {
enter: { duration: 0.1 },
exit: { duration: 0.1 },
};
export default memo(QueueItemComponent);