fix(ui): store customStarUI outside redux

JSX is not serializable, so it cannot be in redux. Non-serializable global state may be put into `nanostores`.

- Use `nanostores` for `customStarUI`
- Use `nanostores` for `headerComponent`
- Re-enable the serializable & immutable check redux middlewares
This commit is contained in:
psychedelicious 2023-09-14 09:02:54 +10:00
parent 2f5e923008
commit f6738d647e
11 changed files with 64 additions and 56 deletions

View File

@ -12,33 +12,26 @@ import { languageSelector } from 'features/system/store/systemSelectors';
import InvokeTabs from 'features/ui/components/InvokeTabs'; import InvokeTabs from 'features/ui/components/InvokeTabs';
import i18n from 'i18n'; import i18n from 'i18n';
import { size } from 'lodash-es'; import { size } from 'lodash-es';
import { ReactNode, memo, useCallback, useEffect } from 'react'; import { memo, useCallback, useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { usePreselectedImage } from '../../features/parameters/hooks/usePreselectedImage'; import { usePreselectedImage } from '../../features/parameters/hooks/usePreselectedImage';
import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; import AppErrorBoundaryFallback from './AppErrorBoundaryFallback';
import GlobalHotkeys from './GlobalHotkeys'; import GlobalHotkeys from './GlobalHotkeys';
import Toaster from './Toaster'; import Toaster from './Toaster';
import { CustomStarUi } from '../../features/ui/store/uiTypes'; import { useStore } from '@nanostores/react';
import { setCustomStarUi } from '../../features/ui/store/uiSlice'; import { $headerComponent } from 'app/store/nanostores/headerComponent';
const DEFAULT_CONFIG = {}; const DEFAULT_CONFIG = {};
interface Props { interface Props {
config?: PartialAppConfig; config?: PartialAppConfig;
headerComponent?: ReactNode;
selectedImage?: { selectedImage?: {
imageName: string; imageName: string;
action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters'; action: 'sendToImg2Img' | 'sendToCanvas' | 'useAllParameters';
}; };
customStarUi?: CustomStarUi;
} }
const App = ({ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
config = DEFAULT_CONFIG,
headerComponent,
selectedImage,
customStarUi,
}: Props) => {
const language = useAppSelector(languageSelector); const language = useAppSelector(languageSelector);
const logger = useLogger('system'); const logger = useLogger('system');
@ -61,12 +54,6 @@ const App = ({
} }
}, [dispatch, config, logger]); }, [dispatch, config, logger]);
useEffect(() => {
if (customStarUi) {
dispatch(setCustomStarUi(customStarUi));
}
}, [customStarUi, dispatch]);
useEffect(() => { useEffect(() => {
dispatch(appStarted()); dispatch(appStarted());
}, [dispatch]); }, [dispatch]);
@ -75,6 +62,8 @@ const App = ({
handlePreselectedImage(selectedImage); handlePreselectedImage(selectedImage);
}, [handlePreselectedImage, selectedImage]); }, [handlePreselectedImage, selectedImage]);
const headerComponent = useStore($headerComponent);
return ( return (
<ErrorBoundary <ErrorBoundary
onReset={handleReset} onReset={handleReset}

View File

@ -15,7 +15,8 @@ import { socketMiddleware } from 'services/events/middleware';
import Loading from '../../common/components/Loading/Loading'; import Loading from '../../common/components/Loading/Loading';
import '../../i18n'; import '../../i18n';
import AppDndContext from '../../features/dnd/components/AppDndContext'; import AppDndContext from '../../features/dnd/components/AppDndContext';
import { CustomStarUi } from '../../features/ui/store/uiTypes'; import { $customStarUI, CustomStarUi } from 'app/store/nanostores/customStarUI';
import { $headerComponent } from 'app/store/nanostores/headerComponent';
const App = lazy(() => import('./App')); const App = lazy(() => import('./App'));
const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider')); const ThemeLocaleProvider = lazy(() => import('./ThemeLocaleProvider'));
@ -83,18 +84,33 @@ const InvokeAIUI = ({
}; };
}, [apiUrl, token, middleware, projectId]); }, [apiUrl, token, middleware, projectId]);
useEffect(() => {
if (customStarUi) {
$customStarUI.set(customStarUi);
}
return () => {
$customStarUI.set(undefined);
};
}, [customStarUi]);
useEffect(() => {
if (headerComponent) {
$headerComponent.set(headerComponent);
}
return () => {
$headerComponent.set(undefined);
};
}, [headerComponent]);
return ( return (
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<React.Suspense fallback={<Loading />}> <React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider> <ThemeLocaleProvider>
<AppDndContext> <AppDndContext>
<App <App config={config} selectedImage={selectedImage} />
config={config}
headerComponent={headerComponent}
selectedImage={selectedImage}
customStarUi={customStarUi}
/>
</AppDndContext> </AppDndContext>
</ThemeLocaleProvider> </ThemeLocaleProvider>
</React.Suspense> </React.Suspense>

View File

@ -0,0 +1,14 @@
import { MenuItemProps } from '@chakra-ui/react';
import { atom } from 'nanostores';
export type CustomStarUi = {
on: {
icon: MenuItemProps['icon'];
text: string;
};
off: {
icon: MenuItemProps['icon'];
text: string;
};
};
export const $customStarUI = atom<CustomStarUi | undefined>(undefined);

View File

@ -0,0 +1,4 @@
import { atom } from 'nanostores';
import { ReactNode } from 'react';
export const $headerComponent = atom<ReactNode | undefined>(undefined);

View File

@ -0,0 +1,3 @@
/**
* For non-serializable data that needs to be available throughout the app, or when redux is not appropriate, use nanostores.
*/

View File

@ -86,10 +86,7 @@ export const store = configureStore({
.concat(autoBatchEnhancer()); .concat(autoBatchEnhancer());
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ getDefaultMiddleware()
immutableCheck: false,
serializableCheck: false,
})
.concat(api.middleware) .concat(api.middleware)
.concat(dynamicMiddlewares) .concat(dynamicMiddlewares)
.prepend(listenerMiddleware.middleware), .prepend(listenerMiddleware.middleware),

View File

@ -1,4 +1,6 @@
import { MenuItem } from '@chakra-ui/react'; import { MenuItem } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { import {
imagesToChangeSelected, imagesToChangeSelected,
@ -12,12 +14,11 @@ import {
useStarImagesMutation, useStarImagesMutation,
useUnstarImagesMutation, useUnstarImagesMutation,
} from '../../../../services/api/endpoints/images'; } from '../../../../services/api/endpoints/images';
import { uiSelector } from '../../../ui/store/uiSelectors';
const MultipleSelectionMenuItems = () => { const MultipleSelectionMenuItems = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const selection = useAppSelector((state) => state.gallery.selection); const selection = useAppSelector((state) => state.gallery.selection);
const { customStarUi } = useAppSelector(uiSelector); const customStarUi = useStore($customStarUI);
const [starImages] = useStarImagesMutation(); const [starImages] = useStarImagesMutation();
const [unstarImages] = useUnstarImagesMutation(); const [unstarImages] = useUnstarImagesMutation();

View File

@ -1,5 +1,7 @@
import { Flex, MenuItem, Spinner } from '@chakra-ui/react'; import { Flex, MenuItem, Spinner } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { useAppToaster } from 'app/components/Toaster'; import { useAppToaster } from 'app/components/Toaster';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice'; import { setInitialCanvasImage } from 'features/canvas/store/canvasSlice';
import { import {
@ -7,6 +9,7 @@ import {
isModalOpenChanged, isModalOpenChanged,
} from 'features/changeBoardModal/store/slice'; } from 'features/changeBoardModal/store/slice';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
import { workflowLoadRequested } from 'features/nodes/store/actions';
import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters'; import { useRecallParameters } from 'features/parameters/hooks/useRecallParameters';
import { initialImageSelected } from 'features/parameters/store/actions'; import { initialImageSelected } from 'features/parameters/store/actions';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@ -32,10 +35,8 @@ import {
useUnstarImagesMutation, useUnstarImagesMutation,
} from 'services/api/endpoints/images'; } from 'services/api/endpoints/images';
import { ImageDTO } from 'services/api/types'; import { ImageDTO } from 'services/api/types';
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
import { workflowLoadRequested } from 'features/nodes/store/actions';
import { configSelector } from '../../../system/store/configSelectors'; import { configSelector } from '../../../system/store/configSelectors';
import { uiSelector } from '../../../ui/store/uiSelectors'; import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
type SingleSelectionMenuItemsProps = { type SingleSelectionMenuItemsProps = {
imageDTO: ImageDTO; imageDTO: ImageDTO;
@ -51,7 +52,7 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled; const isCanvasEnabled = useFeatureStatus('unifiedCanvas').isFeatureEnabled;
const { shouldFetchMetadataFromApi } = useAppSelector(configSelector); const { shouldFetchMetadataFromApi } = useAppSelector(configSelector);
const { customStarUi } = useAppSelector(uiSelector); const customStarUi = useStore($customStarUI);
const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery( const { metadata, workflow, isLoading } = useGetImageMetadataFromFileQuery(
{ image: imageDTO, shouldFetchMetadataFromApi }, { image: imageDTO, shouldFetchMetadataFromApi },

View File

@ -1,4 +1,6 @@
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import { useStore } from '@nanostores/react';
import { $customStarUI } from 'app/store/nanostores/customStarUI';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImage from 'common/components/IAIDndImage';
import IAIFillSkeleton from 'common/components/IAIFillSkeleton'; import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
@ -10,6 +12,7 @@ import {
} from 'features/dnd/types'; } from 'features/dnd/types';
import { useMultiselect } from 'features/gallery/hooks/useMultiselect'; import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
import { MouseEvent, memo, useCallback, useMemo, useState } from 'react'; import { MouseEvent, memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FaTrash } from 'react-icons/fa'; import { FaTrash } from 'react-icons/fa';
import { MdStar, MdStarBorder } from 'react-icons/md'; import { MdStar, MdStarBorder } from 'react-icons/md';
import { import {
@ -18,8 +21,6 @@ import {
useUnstarImagesMutation, useUnstarImagesMutation,
} from 'services/api/endpoints/images'; } from 'services/api/endpoints/images';
import IAIDndImageIcon from '../../../../common/components/IAIDndImageIcon'; import IAIDndImageIcon from '../../../../common/components/IAIDndImageIcon';
import { uiSelector } from '../../../ui/store/uiSelectors';
import { useTranslation } from 'react-i18next';
interface HoverableImageProps { interface HoverableImageProps {
imageName: string; imageName: string;
@ -35,7 +36,7 @@ const GalleryImage = (props: HoverableImageProps) => {
const { handleClick, isSelected, selection, selectionCount } = const { handleClick, isSelected, selection, selectionCount } =
useMultiselect(imageDTO); useMultiselect(imageDTO);
const { customStarUi } = useAppSelector(uiSelector); const customStarUi = useStore($customStarUI);
const handleDelete = useCallback( const handleDelete = useCallback(
(e: MouseEvent<HTMLButtonElement>) => { (e: MouseEvent<HTMLButtonElement>) => {

View File

@ -4,7 +4,7 @@ import { initialImageChanged } from 'features/parameters/store/generationSlice';
import { SchedulerParam } from 'features/parameters/types/parameterSchemas'; import { SchedulerParam } from 'features/parameters/types/parameterSchemas';
import { setActiveTabReducer } from './extraReducers'; import { setActiveTabReducer } from './extraReducers';
import { InvokeTabName } from './tabMap'; import { InvokeTabName } from './tabMap';
import { CustomStarUi, UIState } from './uiTypes'; import { UIState } from './uiTypes';
export const initialUIState: UIState = { export const initialUIState: UIState = {
activeTab: 0, activeTab: 0,
@ -19,7 +19,6 @@ export const initialUIState: UIState = {
favoriteSchedulers: [], favoriteSchedulers: [],
globalContextMenuCloseTrigger: 0, globalContextMenuCloseTrigger: 0,
panels: {}, panels: {},
customStarUi: undefined,
}; };
export const uiSlice = createSlice({ export const uiSlice = createSlice({
@ -71,9 +70,6 @@ export const uiSlice = createSlice({
) => { ) => {
state.panels[action.payload.name] = action.payload.value; state.panels[action.payload.name] = action.payload.value;
}, },
setCustomStarUi: (state, action: PayloadAction<CustomStarUi>) => {
state.customStarUi = action.payload;
},
}, },
extraReducers(builder) { extraReducers(builder) {
builder.addCase(initialImageChanged, (state) => { builder.addCase(initialImageChanged, (state) => {
@ -95,7 +91,6 @@ export const {
setShouldAutoChangeDimensions, setShouldAutoChangeDimensions,
contextMenusClosed, contextMenusClosed,
panelsChanged, panelsChanged,
setCustomStarUi,
} = uiSlice.actions; } = uiSlice.actions;
export default uiSlice.reducer; export default uiSlice.reducer;

View File

@ -1,4 +1,3 @@
import { MenuItemProps } from '@chakra-ui/react';
import { SchedulerParam } from 'features/parameters/types/parameterSchemas'; import { SchedulerParam } from 'features/parameters/types/parameterSchemas';
export type Coordinates = { export type Coordinates = {
@ -13,17 +12,6 @@ export type Dimensions = {
export type Rect = Coordinates & Dimensions; export type Rect = Coordinates & Dimensions;
export type CustomStarUi = {
on: {
icon: MenuItemProps['icon'];
text: string;
};
off: {
icon: MenuItemProps['icon'];
text: string;
};
};
export interface UIState { export interface UIState {
activeTab: number; activeTab: number;
shouldShowImageDetails: boolean; shouldShowImageDetails: boolean;
@ -37,5 +25,4 @@ export interface UIState {
favoriteSchedulers: SchedulerParam[]; favoriteSchedulers: SchedulerParam[];
globalContextMenuCloseTrigger: number; globalContextMenuCloseTrigger: number;
panels: Record<string, string>; panels: Record<string, string>;
customStarUi?: CustomStarUi;
} }