From c41760a500ea0a16aa3da7a0d883290027ae6646 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Fri, 25 Apr 2025 01:39:08 +0200 Subject: [PATCH] chore: bump zustand (#9577) * bump zustand to v5 * add missing shallow * fix missing shallow --------- Co-authored-by: Oliver --- src/frontend/package.json | 2 +- src/frontend/src/components/SplashScreen.tsx | 8 +++--- .../src/components/calendar/Calendar.tsx | 5 ++-- .../components/dashboard/DashboardLayout.tsx | 15 ++++++----- .../src/components/forms/AuthFormOptions.tsx | 3 ++- .../components/forms/AuthenticationForm.tsx | 25 +++++++++++-------- .../src/components/forms/InstanceOptions.tsx | 11 ++++---- .../src/components/forms/fields/IconField.tsx | 3 ++- src/frontend/src/components/items/ApiIcon.tsx | 5 +++- .../src/components/items/LanguageSelect.tsx | 8 +++--- .../src/components/items/OnlyStaff.tsx | 3 ++- .../components/modals/AboutInvenTreeModal.tsx | 20 ++++++++++++--- .../src/components/modals/ServerInfoModal.tsx | 3 ++- src/frontend/src/components/nav/Alerts.tsx | 3 ++- .../src/components/nav/DetailDrawer.tsx | 12 +++++---- src/frontend/src/components/nav/Header.tsx | 19 +++++++------- src/frontend/src/components/nav/MainMenu.tsx | 8 +++--- .../src/components/panels/PanelGroup.tsx | 25 +++++++++++-------- .../src/components/plugins/PluginContext.tsx | 4 +-- src/frontend/src/contexts/LanguageContext.tsx | 5 ++-- src/frontend/src/contexts/ThemeContext.tsx | 3 ++- src/frontend/src/defaults/actions.tsx | 5 +++- src/frontend/src/forms/StockForms.tsx | 10 +++++--- src/frontend/src/pages/Auth/Login.tsx | 22 ++++++++-------- .../AccountSettings/AccountDetailPanel.tsx | 8 +++--- .../AccountSettings/SecurityContent.tsx | 12 +++++---- .../AccountSettings/UserThemePanel.tsx | 9 +++---- .../AdminCenter/PluginManagementPanel.tsx | 3 ++- .../pages/Index/Settings/SystemSettings.tsx | 3 ++- .../src/pages/Index/Settings/UserSettings.tsx | 8 +++--- .../src/tables/settings/UserTable.tsx | 4 +-- src/frontend/src/views/DesktopAppView.tsx | 3 ++- src/frontend/src/views/MainView.tsx | 5 +++- src/frontend/src/views/MobileAppView.tsx | 5 +++- src/frontend/yarn.lock | 15 +++-------- 35 files changed, 175 insertions(+), 127 deletions(-) diff --git a/src/frontend/package.json b/src/frontend/package.json index 169a4ec98d..4ae8c80ab8 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -101,7 +101,7 @@ "react-window": "1.8.10", "recharts": "^2.15.0", "styled-components": "^6.1.14", - "zustand": "^4.5.5" + "zustand": "^5.0.3" }, "devDependencies": { "@babel/core": "^7.26.10", diff --git a/src/frontend/src/components/SplashScreen.tsx b/src/frontend/src/components/SplashScreen.tsx index 8c8e783d57..54ef01f203 100644 --- a/src/frontend/src/components/SplashScreen.tsx +++ b/src/frontend/src/components/SplashScreen.tsx @@ -1,5 +1,6 @@ import { BackgroundImage } from '@mantine/core'; import { useEffect } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { generateUrl } from '../functions/urls'; import { useServerApiState } from '../states/ApiState'; @@ -11,10 +12,9 @@ export default function SplashScreen({ }: Readonly<{ children: React.ReactNode; }>) { - const [server, fetchServerApiState] = useServerApiState((state) => [ - state.server, - state.fetchServerApiState - ]); + const [server, fetchServerApiState] = useServerApiState( + useShallow((state) => [state.server, state.fetchServerApiState]) + ); // Fetch server data on mount if no server data is present useEffect(() => { diff --git a/src/frontend/src/components/calendar/Calendar.tsx b/src/frontend/src/components/calendar/Calendar.tsx index a088a8c1f1..de1068398a 100644 --- a/src/frontend/src/components/calendar/Calendar.tsx +++ b/src/frontend/src/components/calendar/Calendar.tsx @@ -26,6 +26,7 @@ import { IconFilter } from '@tabler/icons-react'; import { useCallback, useState } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import type { CalendarState } from '../../hooks/UseCalendar'; import { useLocalState } from '../../states/LocalState'; import { FilterSelectDrawer } from '../../tables/FilterSelectDrawer'; @@ -51,12 +52,12 @@ export default function Calendar({ filters, state, ...calendarProps -}: InvenTreeCalendarProps) { +}: Readonly) { const [monthSelectOpened, setMonthSelectOpened] = useState(false); const [filtersVisible, setFiltersVisible] = useState(false); - const [locale] = useLocalState((s) => [s.language]); + const [locale] = useLocalState(useShallow((s) => [s.language])); const selectMonth = useCallback( (date: DateValue) => { diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index 8b7be0a732..8d1cbc0352 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -5,6 +5,7 @@ import { IconInfoCircle } from '@tabler/icons-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { type Layout, Responsive, WidthProvider } from 'react-grid-layout'; +import { useShallow } from 'zustand/react/shallow'; import { useDashboardItems } from '../../hooks/UseDashboardItems'; import { useLocalState } from '../../states/LocalState'; import DashboardMenu from './DashboardMenu'; @@ -21,12 +22,14 @@ export default function DashboardLayout() { // local/remote storage values for widget / layout const [remoteWidgets, setRemoteWidgets, remoteLayouts, setRemoteLayouts] = - useLocalState((state) => [ - state.widgets, - state.setWidgets, - state.layouts, - state.setLayouts - ]); + useLocalState( + useShallow((state) => [ + state.widgets, + state.setWidgets, + state.layouts, + state.setLayouts + ]) + ); const [editing, setEditing] = useDisclosure(false); const [removing, setRemoving] = useDisclosure(false); diff --git a/src/frontend/src/components/forms/AuthFormOptions.tsx b/src/frontend/src/components/forms/AuthFormOptions.tsx index 7b6e34d90f..8c23313b90 100644 --- a/src/frontend/src/components/forms/AuthFormOptions.tsx +++ b/src/frontend/src/components/forms/AuthFormOptions.tsx @@ -1,6 +1,7 @@ import { ActionIcon, Center, Group, Text, Tooltip } from '@mantine/core'; import { IconServer } from '@tabler/icons-react'; +import { useShallow } from 'zustand/react/shallow'; import { useServerApiState } from '../../states/ApiState'; import { ColorToggle } from '../items/ColorToggle'; import { LanguageToggle } from '../items/LanguageToggle'; @@ -12,7 +13,7 @@ export function AuthFormOptions({ hostname: string; toggleHostEdit: () => void; }>) { - const [server] = useServerApiState((state) => [state.server]); + const [server] = useServerApiState(useShallow((state) => [state.server])); return (
diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx index 12652f705d..f3673700a8 100644 --- a/src/frontend/src/components/forms/AuthenticationForm.tsx +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -18,6 +18,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; import { showNotification } from '@mantine/notifications'; +import { useShallow } from 'zustand/react/shallow'; import { api } from '../../App'; import { doBasicLogin, @@ -37,11 +38,13 @@ export function AuthenticationForm() { const simpleForm = useForm({ initialValues: { email: '' } }); const [classicLoginMode, setMode] = useDisclosure(true); const [auth_config, sso_enabled, password_forgotten_enabled] = - useServerApiState((state) => [ - state.auth_config, - state.sso_enabled, - state.password_forgotten_enabled - ]); + useServerApiState( + useShallow((state) => [ + state.auth_config, + state.sso_enabled, + state.password_forgotten_enabled + ]) + ); const navigate = useNavigate(); const location = useLocation(); const { isLoggedIn } = useUserState(); @@ -207,11 +210,13 @@ export function RegistrationForm() { }); const navigate = useNavigate(); const [auth_config, registration_enabled, sso_registration] = - useServerApiState((state) => [ - state.auth_config, - state.registration_enabled, - state.sso_registration_enabled - ]); + useServerApiState( + useShallow((state) => [ + state.auth_config, + state.registration_enabled, + state.sso_registration_enabled + ]) + ); const [isRegistering, setIsRegistering] = useState(false); async function handleRegistration() { diff --git a/src/frontend/src/components/forms/InstanceOptions.tsx b/src/frontend/src/components/forms/InstanceOptions.tsx index 22adc431c7..f38e7ec0e3 100644 --- a/src/frontend/src/components/forms/InstanceOptions.tsx +++ b/src/frontend/src/components/forms/InstanceOptions.tsx @@ -21,6 +21,7 @@ import { } from '@tabler/icons-react'; import type { HostList } from '@lib/types/Server'; +import { useShallow } from 'zustand/react/shallow'; import { Wrapper } from '../../pages/Auth/Layout'; import { useServerApiState } from '../../states/ApiState'; import { useLocalState } from '../../states/LocalState'; @@ -37,11 +38,9 @@ export function InstanceOptions({ setHostEdit: () => void; }>) { const [hostListEdit, setHostListEdit] = useToggle([false, true] as const); - const [setHost, setHostList, hostList] = useLocalState((state) => [ - state.setHost, - state.setHostList, - state.hostList - ]); + const [setHost, setHostList, hostList] = useLocalState( + useShallow((state) => [state.setHost, state.setHostList, state.hostList]) + ); const hostListData = Object.keys(hostList).map((key) => ({ value: key, label: hostList[key]?.name @@ -111,7 +110,7 @@ function ServerInfo({ hostList: HostList; hostKey: string; }>) { - const [server] = useServerApiState((state) => [state.server]); + const [server] = useServerApiState(useShallow((state) => [state.server])); const items: any[] = [ { diff --git a/src/frontend/src/components/forms/fields/IconField.tsx b/src/frontend/src/components/forms/fields/IconField.tsx index 590a18ab82..980b508733 100644 --- a/src/frontend/src/components/forms/fields/IconField.tsx +++ b/src/frontend/src/components/forms/fields/IconField.tsx @@ -22,6 +22,7 @@ import type { FieldValues, UseControllerReturn } from 'react-hook-form'; import { FixedSizeGrid as Grid } from 'react-window'; import type { ApiFormFieldType } from '@lib/types/Forms'; +import { useShallow } from 'zustand/react/shallow'; import { useIconState } from '../../../states/IconState'; import { ApiIcon } from '../../items/ApiIcon'; @@ -119,7 +120,7 @@ function ComboboxDropdown({ onChange: (newVal: string | null) => void; open: boolean; }>) { - const iconPacks = useIconState((s) => s.packages); + const iconPacks = useIconState(useShallow((s) => s.packages)); const icons = useMemo(() => { return iconPacks.flatMap((pack) => Object.entries(pack.icons).flatMap(([name, icon]) => diff --git a/src/frontend/src/components/items/ApiIcon.tsx b/src/frontend/src/components/items/ApiIcon.tsx index 58e02662a1..5ad67ab61c 100644 --- a/src/frontend/src/components/items/ApiIcon.tsx +++ b/src/frontend/src/components/items/ApiIcon.tsx @@ -1,3 +1,4 @@ +import { useShallow } from 'zustand/react/shallow'; import { useIconState } from '../../states/IconState'; import * as classes from './ApiIcon.css'; @@ -9,7 +10,9 @@ type ApiIconProps = { export const ApiIcon = ({ name: _name, size = 22 }: ApiIconProps) => { const [iconPackage, name, variant] = _name.split(':'); const icon = useIconState( - (s) => s.packagesMap[iconPackage]?.icons[name]?.variants[variant] + useShallow( + (s) => s.packagesMap[iconPackage]?.icons[name]?.variants[variant] + ) ); const unicode = icon ? String.fromCodePoint(Number.parseInt(icon, 16)) : ''; diff --git a/src/frontend/src/components/items/LanguageSelect.tsx b/src/frontend/src/components/items/LanguageSelect.tsx index a7a062c714..6ef3cdfc0a 100644 --- a/src/frontend/src/components/items/LanguageSelect.tsx +++ b/src/frontend/src/components/items/LanguageSelect.tsx @@ -1,15 +1,15 @@ import { Select } from '@mantine/core'; import { useEffect, useState } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { getSupportedLanguages } from '../../contexts/LanguageContext'; import { useLocalState } from '../../states/LocalState'; export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) { const [value, setValue] = useState(null); - const [locale, setLanguage] = useLocalState((state) => [ - state.language, - state.setLanguage - ]); + const [locale, setLanguage] = useLocalState( + useShallow((state) => [state.language, state.setLanguage]) + ); const [langOptions, setLangOptions] = useState([]); // change global language on change diff --git a/src/frontend/src/components/items/OnlyStaff.tsx b/src/frontend/src/components/items/OnlyStaff.tsx index 74b9001e62..192e748d53 100644 --- a/src/frontend/src/components/items/OnlyStaff.tsx +++ b/src/frontend/src/components/items/OnlyStaff.tsx @@ -1,9 +1,10 @@ import { Trans } from '@lingui/react/macro'; +import { useShallow } from 'zustand/react/shallow'; import { useUserState } from '../../states/UserState'; export const OnlyStaff = ({ children }: { children: any }) => { - const [user] = useUserState((state) => [state.user]); + const [user] = useUserState(useShallow((state) => [state.user])); if (user?.is_staff) return children; return This information is only available for staff users; diff --git a/src/frontend/src/components/modals/AboutInvenTreeModal.tsx b/src/frontend/src/components/modals/AboutInvenTreeModal.tsx index 30abd00b3b..a8c7e6aeed 100644 --- a/src/frontend/src/components/modals/AboutInvenTreeModal.tsx +++ b/src/frontend/src/components/modals/AboutInvenTreeModal.tsx @@ -16,6 +16,7 @@ import { useQuery } from '@tanstack/react-query'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; +import { useShallow } from 'zustand/react/shallow'; import { api } from '../../App'; import { generateUrl } from '../../functions/urls'; import { useServerApiState } from '../../states/ApiState'; @@ -32,14 +33,14 @@ type AboutLookupRef = { export function AboutInvenTreeModal({ context, - id + id, + innerProps }: Readonly< ContextModalProps<{ modalBody: string; }> >) { - const [user] = useUserState((state) => [state.user]); - const [server] = useServerApiState((state) => [state.server]); + const [user] = useUserState(useShallow((state) => [state.user])); if (!user?.is_staff) return ( @@ -47,7 +48,18 @@ export function AboutInvenTreeModal({ This information is only available for staff users ); + return ; +} +const AboutContent = ({ + context, + id +}: Readonly< + ContextModalProps<{ + modalBody: string; + }> +>) => { + const [server] = useServerApiState(useShallow((state) => [state.server])); const { isLoading, data } = useQuery({ queryKey: ['version'], queryFn: () => api.get(apiUrl(ApiEndpoints.version)).then((res) => res.data) @@ -185,7 +197,7 @@ export function AboutInvenTreeModal({ ); -} +}; function renderVersionBadge(data: any) { const badgeType = () => { diff --git a/src/frontend/src/components/modals/ServerInfoModal.tsx b/src/frontend/src/components/modals/ServerInfoModal.tsx index b58b0c3fdd..9633dbab68 100644 --- a/src/frontend/src/components/modals/ServerInfoModal.tsx +++ b/src/frontend/src/components/modals/ServerInfoModal.tsx @@ -2,6 +2,7 @@ import { Trans } from '@lingui/react/macro'; import { Badge, Button, Divider, Group, Stack, Table } from '@mantine/core'; import type { ContextModalProps } from '@mantine/modals'; +import { useShallow } from 'zustand/react/shallow'; import { useServerApiState } from '../../states/ApiState'; import { OnlyStaff } from '../items/OnlyStaff'; import { StylishText } from '../items/StylishText'; @@ -10,7 +11,7 @@ export function ServerInfoModal({ context, id }: ContextModalProps<{ modalBody: string }>) { - const [server] = useServerApiState((state) => [state.server]); + const [server] = useServerApiState(useShallow((state) => [state.server])); return ( diff --git a/src/frontend/src/components/nav/Alerts.tsx b/src/frontend/src/components/nav/Alerts.tsx index 822e68d6ee..5e82326a06 100644 --- a/src/frontend/src/components/nav/Alerts.tsx +++ b/src/frontend/src/components/nav/Alerts.tsx @@ -3,6 +3,7 @@ import { IconExclamationCircle } from '@tabler/icons-react'; import { useMemo, useState } from 'react'; import { t } from '@lingui/core/macro'; +import { useShallow } from 'zustand/react/shallow'; import { docLinks } from '../../defaults/links'; import { useServerApiState } from '../../states/ApiState'; import { useGlobalSettingsState } from '../../states/SettingsState'; @@ -26,7 +27,7 @@ interface AlertInfo { */ export function Alerts() { const user = useUserState(); - const [server] = useServerApiState((state) => [state.server]); + const [server] = useServerApiState(useShallow((state) => [state.server])); const globalSettings = useGlobalSettingsState(); const [dismissed, setDismissed] = useState([]); diff --git a/src/frontend/src/components/nav/DetailDrawer.tsx b/src/frontend/src/components/nav/DetailDrawer.tsx index d47fd4e1ab..dcc7d77e6a 100644 --- a/src/frontend/src/components/nav/DetailDrawer.tsx +++ b/src/frontend/src/components/nav/DetailDrawer.tsx @@ -5,6 +5,7 @@ import { Link, Route, Routes, useNavigate, useParams } from 'react-router-dom'; import type { To } from 'react-router-dom'; import type { UiSizeType } from '@lib/types/Core'; +import { useShallow } from 'zustand/react/shallow'; import { useLocalState } from '../../states/LocalState'; import { StylishText } from '../items/StylishText'; import * as classes from './DetailDrawer.css'; @@ -37,10 +38,9 @@ function DetailDrawerComponent({ const content = renderContent(id); const opened = useMemo(() => !!id && !!content, [id, content]); - const [detailDrawerStack, addDetailDrawer] = useLocalState((state) => [ - state.detailDrawerStack, - state.addDetailDrawer - ]); + const [detailDrawerStack, addDetailDrawer] = useLocalState( + useShallow((state) => [state.detailDrawerStack, state.addDetailDrawer]) + ); return ( ) { - const addDetailDrawer = useLocalState((state) => state.addDetailDrawer); + const addDetailDrawer = useLocalState( + useShallow((state) => state.addDetailDrawer) + ); const onNavigate = useCallback(() => { addDetailDrawer(1); diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index b85a49e0d7..524ac8a144 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -1,7 +1,3 @@ -import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; -import { apiUrl } from '@lib/functions/Api'; -import { navigateToLink } from '@lib/functions/Navigation'; -import { t } from '@lingui/core/macro'; import { ActionIcon, Container, @@ -16,6 +12,12 @@ import { IconBell, IconSearch } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { type ReactNode, useEffect, useMemo, useState } from 'react'; import { useMatch, useNavigate } from 'react-router-dom'; + +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; +import { apiUrl } from '@lib/functions/Api'; +import { navigateToLink } from '@lib/functions/Navigation'; +import { t } from '@lingui/core/macro'; +import { useShallow } from 'zustand/react/shallow'; import { api } from '../../App'; import type { NavigationUIFeature } from '../../components/plugins/PluginUIFeatureTypes'; import { getNavTabs } from '../../defaults/links'; @@ -38,11 +40,10 @@ import { NotificationDrawer } from './NotificationDrawer'; import { SearchDrawer } from './SearchDrawer'; export function Header() { - const [setNavigationOpen, navigationOpen] = useLocalState((state) => [ - state.setNavigationOpen, - state.navigationOpen - ]); - const [server] = useServerApiState((state) => [state.server]); + const [setNavigationOpen, navigationOpen] = useLocalState( + useShallow((state) => [state.setNavigationOpen, state.navigationOpen]) + ); + const [server] = useServerApiState(useShallow((state) => [state.server])); const [navDrawerOpened, { open: openNavDrawer, close: closeNavDrawer }] = useDisclosure(navigationOpen); const [ diff --git a/src/frontend/src/components/nav/MainMenu.tsx b/src/frontend/src/components/nav/MainMenu.tsx index 964e256310..9f77889a0e 100644 --- a/src/frontend/src/components/nav/MainMenu.tsx +++ b/src/frontend/src/components/nav/MainMenu.tsx @@ -18,6 +18,7 @@ import { } from '@tabler/icons-react'; import { Link, useNavigate } from 'react-router-dom'; +import { useShallow } from 'zustand/react/shallow'; import { doLogout } from '../../functions/auth'; import * as classes from '../../main.css'; import { useUserState } from '../../states/UserState'; @@ -25,10 +26,9 @@ import { vars } from '../../theme'; export function MainMenu() { const navigate = useNavigate(); - const [user, username] = useUserState((state) => [ - state.user, - state.username - ]); + const [user, username] = useUserState( + useShallow((state) => [state.user, state.username]) + ); const { colorScheme, toggleColorScheme } = useMantineColorScheme(); return ( diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx index 2928cf25a7..f3688c8c87 100644 --- a/src/frontend/src/components/panels/PanelGroup.tsx +++ b/src/frontend/src/components/panels/PanelGroup.tsx @@ -31,6 +31,7 @@ import { import type { ModelType } from '@lib/enums/ModelType'; import { cancelEvent } from '@lib/functions/Events'; import { navigateToLink } from '@lib/functions/Navigation'; +import { useShallow } from 'zustand/react/shallow'; import { identifierString } from '../../functions/conversion'; import { usePluginPanels } from '../../hooks/UsePluginPanels'; import { useLocalState } from '../../states/LocalState'; @@ -262,19 +263,21 @@ function IndexPanelComponent({ selectedPanel, panels }: Readonly) { - const lastUsedPanel = useLocalState((state) => { - const panelName = - selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name; + const lastUsedPanel = useLocalState( + useShallow((state) => { + const panelName = + selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name; - const panel = panels.findIndex( - (p) => p.name === panelName && !p.disabled && !p.hidden - ); - if (panel === -1) { - return panels.find((p) => !p.disabled && !p.hidden)?.name || ''; - } + const panel = panels.findIndex( + (p) => p.name === panelName && !p.disabled && !p.hidden + ); + if (panel === -1) { + return panels.find((p) => !p.disabled && !p.hidden)?.name || ''; + } - return panelName; - }); + return panelName; + }) + ); return ; } diff --git a/src/frontend/src/components/plugins/PluginContext.tsx b/src/frontend/src/components/plugins/PluginContext.tsx index d6451f0666..fea256c204 100644 --- a/src/frontend/src/components/plugins/PluginContext.tsx +++ b/src/frontend/src/components/plugins/PluginContext.tsx @@ -1,7 +1,7 @@ import { useMantineColorScheme, useMantineTheme } from '@mantine/core'; import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; - +import { useShallow } from 'zustand/react/shallow'; import { api, queryClient } from '../../App'; import { useLocalState } from '../../states/LocalState'; import { @@ -24,7 +24,7 @@ import { } from '../../hooks/UseForm'; export const useInvenTreeContext = () => { - const [locale, host] = useLocalState((s) => [s.language, s.host]); + const [locale, host] = useLocalState(useShallow((s) => [s.language, s.host])); const navigate = useNavigate(); const user = useUserState(); const { colorScheme } = useMantineColorScheme(); diff --git a/src/frontend/src/contexts/LanguageContext.tsx b/src/frontend/src/contexts/LanguageContext.tsx index 35b666268c..9923c12093 100644 --- a/src/frontend/src/contexts/LanguageContext.tsx +++ b/src/frontend/src/contexts/LanguageContext.tsx @@ -4,6 +4,7 @@ import { I18nProvider } from '@lingui/react'; import { LoadingOverlay, Text } from '@mantine/core'; import { useEffect, useRef, useState } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { api } from '../App'; import { useServerApiState } from '../states/ApiState'; import { useLocalState } from '../states/LocalState'; @@ -61,8 +62,8 @@ export const getSupportedLanguages = (): Record => { export function LanguageContext({ children }: Readonly<{ children: JSX.Element }>) { - const [language] = useLocalState((state) => [state.language]); - const [server] = useServerApiState((state) => [state.server]); + const [language] = useLocalState(useShallow((state) => [state.language])); + const [server] = useServerApiState(useShallow((state) => [state.server])); useEffect(() => { activateLocale(defaultLocale); diff --git a/src/frontend/src/contexts/ThemeContext.tsx b/src/frontend/src/contexts/ThemeContext.tsx index 6373c660b2..7fa0f7fb3e 100644 --- a/src/frontend/src/contexts/ThemeContext.tsx +++ b/src/frontend/src/contexts/ThemeContext.tsx @@ -4,6 +4,7 @@ import { MantineProvider, createTheme } from '@mantine/core'; import { ModalsProvider } from '@mantine/modals'; import { Notifications } from '@mantine/notifications'; import { ContextMenuProvider } from 'mantine-contextmenu'; +import { useShallow } from 'zustand/react/shallow'; import { AboutInvenTreeModal } from '../components/modals/AboutInvenTreeModal'; import { LicenseModal } from '../components/modals/LicenseModal'; import { QrModal } from '../components/modals/QrModal'; @@ -15,7 +16,7 @@ import { colorSchema } from './colorSchema'; export function ThemeContext({ children }: Readonly<{ children: JSX.Element }>) { - const [userTheme] = useLocalState((state) => [state.userTheme]); + const [userTheme] = useLocalState(useShallow((state) => [state.userTheme])); // Theme const myTheme = createTheme({ diff --git a/src/frontend/src/defaults/actions.tsx b/src/frontend/src/defaults/actions.tsx index 6164c5e847..699b210630 100644 --- a/src/frontend/src/defaults/actions.tsx +++ b/src/frontend/src/defaults/actions.tsx @@ -4,6 +4,7 @@ import { IconBarcode, IconLink, IconPointer } from '@tabler/icons-react'; import type { NavigateFunction } from 'react-router-dom'; import { openContextModal } from '@mantine/modals'; +import { useShallow } from 'zustand/react/shallow'; import { useLocalState } from '../states/LocalState'; import { useUserState } from '../states/UserState'; import { aboutInvenTree, docLinks, licenseInfo, serverInfo } from './links'; @@ -16,7 +17,9 @@ export function openQrModal(navigate: NavigateFunction) { } export function getActions(navigate: NavigateFunction) { - const setNavigationOpen = useLocalState((state) => state.setNavigationOpen); + const setNavigationOpen = useLocalState( + useShallow((state) => state.setNavigationOpen) + ); const { user } = useUserState(); const actions: SpotlightActionData[] = [ diff --git a/src/frontend/src/forms/StockForms.tsx b/src/frontend/src/forms/StockForms.tsx index dc6c7a3bf7..16d64aa528 100644 --- a/src/frontend/src/forms/StockForms.tsx +++ b/src/frontend/src/forms/StockForms.tsx @@ -500,15 +500,19 @@ function StockOperationsRow({ props.removeFn(props.idx); }; + const callChangeFn = (idx: number, key: string, value: any) => { + setTimeout(() => props.changeFn(idx, key, value), 0); + }; + const [packagingOpen, packagingHandlers] = useDisclosure(false, { onOpen: () => { if (transfer) { - props.changeFn(props.idx, 'packaging', record?.packaging || undefined); + callChangeFn(props.idx, 'packaging', record?.packaging || undefined); } }, onClose: () => { if (transfer) { - props.changeFn(props.idx, 'packaging', undefined); + callChangeFn(props.idx, 'packaging', undefined); } } }); @@ -520,7 +524,7 @@ function StockOperationsRow({ }, onClose: () => { setStatus(undefined); - props.changeFn(props.idx, 'status', undefined); + callChangeFn(props.idx, 'status', undefined); } }); diff --git a/src/frontend/src/pages/Auth/Login.tsx b/src/frontend/src/pages/Auth/Login.tsx index 2a16d35968..a8c9e777e3 100644 --- a/src/frontend/src/pages/Auth/Login.tsx +++ b/src/frontend/src/pages/Auth/Login.tsx @@ -4,6 +4,8 @@ import { Anchor, Divider, Group, Loader, Text } from '@mantine/core'; import { useToggle } from '@mantine/hooks'; import { useEffect, useMemo, useState } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; + +import { useShallow } from 'zustand/react/shallow'; import { setApiDefaults } from '../../App'; import { AuthFormOptions } from '../../components/forms/AuthFormOptions'; import { AuthenticationForm } from '../../components/forms/AuthenticationForm'; @@ -19,15 +21,12 @@ import { useLocalState } from '../../states/LocalState'; import { Wrapper } from './Layout'; export default function Login() { - const [hostKey, setHost, hostList] = useLocalState((state) => [ - state.hostKey, - state.setHost, - state.hostList - ]); - const [server, fetchServerApiState] = useServerApiState((state) => [ - state.server, - state.fetchServerApiState - ]); + const [hostKey, setHost, hostList] = useLocalState( + useShallow((state) => [state.hostKey, state.setHost, state.hostList]) + ); + const [server, fetchServerApiState] = useServerApiState( + useShallow((state) => [state.server, state.fetchServerApiState]) + ); const [isLoggingIn, setIsLoggingIn] = useState(false); const hostname = hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey]?.name; @@ -36,7 +35,10 @@ export default function Login() { const location = useLocation(); const [searchParams] = useSearchParams(); const [sso_registration, registration_enabled] = useServerApiState( - (state) => [state.sso_registration_enabled, state.registration_enabled] + useShallow((state) => [ + state.sso_registration_enabled, + state.registration_enabled + ]) ); const both_reg_enabled = registration_enabled() || sso_registration() || false; diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx index 4cfa95179e..fb4356b036 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/AccountDetailPanel.tsx @@ -7,6 +7,7 @@ import { useMemo } from 'react'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import type { ApiFormFieldSet } from '@lib/types/Forms'; import { useNavigate } from 'react-router-dom'; +import { useShallow } from 'zustand/react/shallow'; import { ActionButton } from '../../../../components/buttons/ActionButton'; import { YesNoUndefinedButton } from '../../../../components/buttons/YesNoButton'; import { ActionDropdown } from '../../../../components/items/ActionDropdown'; @@ -17,10 +18,9 @@ import { useUserState } from '../../../../states/UserState'; export function AccountDetailPanel() { const navigate = useNavigate(); - const [user, fetchUserState] = useUserState((state) => [ - state.user, - state.fetchUserState - ]); + const [user, fetchUserState] = useUserState( + useShallow((state) => [state.user, state.fetchUserState]) + ); const userFields: ApiFormFieldSet = useMemo(() => { return { diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx index fbaf3a6e76..f03a181215 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/SecurityContent.tsx @@ -31,6 +31,7 @@ import { } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { api } from '../../../../App'; import { StylishText } from '../../../../components/items/StylishText'; import { ProviderLogin, authApi } from '../../../../functions/auth'; @@ -40,10 +41,9 @@ import { QrRegistrationForm } from './QrRegistrationForm'; import { useReauth } from './useConfirm'; export function SecurityContent() { - const [auth_config, sso_enabled] = useServerApiState((state) => [ - state.auth_config, - state.sso_enabled - ]); + const [auth_config, sso_enabled] = useServerApiState( + useShallow((state) => [state.auth_config, state.sso_enabled]) + ); return ( @@ -510,7 +510,9 @@ function MfaAddSection({ refetch: () => void; showRecoveryCodes: (codes: Recoverycodes) => void; }>) { - const [auth_config] = useServerApiState((state) => [state.auth_config]); + const [auth_config] = useServerApiState( + useShallow((state) => [state.auth_config]) + ); const [totpQrOpen, { open: openTotpQr, close: closeTotpQr }] = useDisclosure(false); const [totpQr, setTotpQr] = useState<{ totp_url: string; secret: string }>(); diff --git a/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx b/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx index da8748da75..c0d8394d64 100644 --- a/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AccountSettings/UserThemePanel.tsx @@ -17,6 +17,7 @@ import { import { IconRestore } from '@tabler/icons-react'; import { useState } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { ColorToggle } from '../../../../components/items/ColorToggle'; import { LanguageSelect } from '../../../../components/items/LanguageSelect'; import { StylishText } from '../../../../components/items/StylishText'; @@ -34,11 +35,9 @@ const LOOKUP = Object.assign( export function UserTheme({ height }: Readonly<{ height: number }>) { const theme = useMantineTheme(); - const [userTheme, setTheme, setLanguage] = useLocalState((state) => [ - state.userTheme, - state.setTheme, - state.setLanguage - ]); + const [userTheme, setTheme, setLanguage] = useLocalState( + useShallow((state) => [state.userTheme, state.setTheme, state.setLanguage]) + ); // radius function getMark(value: number) { diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/PluginManagementPanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/PluginManagementPanel.tsx index 3b0a584570..d20fdd4182 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/PluginManagementPanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/PluginManagementPanel.tsx @@ -4,6 +4,7 @@ import { Accordion, Alert, Stack } from '@mantine/core'; import { IconInfoCircle } from '@tabler/icons-react'; import { lazy } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { StylishText } from '../../../../components/items/StylishText'; import { GlobalSettingList } from '../../../../components/settings/SettingList'; import { Loadable } from '../../../../functions/loading'; @@ -20,7 +21,7 @@ const PluginErrorTable = Loadable( export default function PluginManagementPanel() { const pluginsEnabled = useServerApiState( - (state) => state.server.plugins_enabled + useShallow((state) => state.server.plugins_enabled) ); const user = useUserState(); diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index 7cf65b7fb0..e77831655a 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -18,6 +18,7 @@ import { } from '@tabler/icons-react'; import { useMemo } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import PermissionDenied from '../../../components/errors/PermissionDenied'; import PageTitle from '../../../components/nav/PageTitle'; import { SettingsHeader } from '../../../components/nav/SettingsHeader'; @@ -309,7 +310,7 @@ export default function SystemSettings() { const user = useUserState(); - const [server] = useServerApiState((state) => [state.server]); + const [server] = useServerApiState(useShallow((state) => [state.server])); if (!user.isLoggedIn()) { return ; diff --git a/src/frontend/src/pages/Index/Settings/UserSettings.tsx b/src/frontend/src/pages/Index/Settings/UserSettings.tsx index 84e8e2f808..c82ad27972 100644 --- a/src/frontend/src/pages/Index/Settings/UserSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/UserSettings.tsx @@ -10,6 +10,7 @@ import { } from '@tabler/icons-react'; import { useMemo } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import PageTitle from '../../../components/nav/PageTitle'; import { SettingsHeader } from '../../../components/nav/SettingsHeader'; import type { PanelType } from '../../../components/panels/Panel'; @@ -23,10 +24,9 @@ import { AccountContent } from './AccountSettings/UserPanel'; * User settings page */ export default function UserSettings() { - const [user, isLoggedIn] = useUserState((state) => [ - state.user, - state.isLoggedIn - ]); + const [user, isLoggedIn] = useUserState( + useShallow((state) => [state.user, state.isLoggedIn]) + ); const userSettingsPanels: PanelType[] = useMemo(() => { return [ diff --git a/src/frontend/src/tables/settings/UserTable.tsx b/src/frontend/src/tables/settings/UserTable.tsx index 04059d96d4..e88812c214 100644 --- a/src/frontend/src/tables/settings/UserTable.tsx +++ b/src/frontend/src/tables/settings/UserTable.tsx @@ -11,6 +11,7 @@ import { apiUrl } from '@lib/functions/Api'; import type { TableFilter } from '@lib/types/Filters'; import { showNotification } from '@mantine/notifications'; import { useNavigate } from 'react-router-dom'; +import { useShallow } from 'zustand/react/shallow'; import { api } from '../../App'; import { AddItemButton } from '../../components/buttons/AddItemButton'; import { EditApiForm } from '../../components/forms/ApiForm'; @@ -63,8 +64,7 @@ export function UserDrawer({ throwError: true }); - const currentUserPk = useUserState((s) => s.user?.pk); - + const currentUserPk = useUserState(useShallow((s) => s.user?.pk)); const isCurrentUser = useMemo( () => currentUserPk === Number.parseInt(id, 10), [currentUserPk, id] diff --git a/src/frontend/src/views/DesktopAppView.tsx b/src/frontend/src/views/DesktopAppView.tsx index 05f017d99d..ba92181144 100644 --- a/src/frontend/src/views/DesktopAppView.tsx +++ b/src/frontend/src/views/DesktopAppView.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { BrowserRouter } from 'react-router-dom'; import { getBaseUrl } from '@lib/functions/Navigation'; +import { useShallow } from 'zustand/react/shallow'; import { api, queryClient } from '../App'; import { ApiProvider } from '../contexts/ApiContext'; import { ThemeContext } from '../contexts/ThemeContext'; @@ -10,7 +11,7 @@ import { routes } from '../router'; import { useLocalState } from '../states/LocalState'; export default function DesktopAppView() { - const [hostList] = useLocalState((state) => [state.hostList]); + const [hostList] = useLocalState(useShallow((state) => [state.hostList])); useEffect(() => { if (Object.keys(hostList).length === 0) { diff --git a/src/frontend/src/views/MainView.tsx b/src/frontend/src/views/MainView.tsx index c874ae7288..a79611af55 100644 --- a/src/frontend/src/views/MainView.tsx +++ b/src/frontend/src/views/MainView.tsx @@ -1,6 +1,7 @@ import '@mantine/core/styles.css'; import { useViewportSize } from '@mantine/hooks'; import { lazy, useEffect } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { setApiDefaults } from '../App'; import { Loadable } from '../functions/loading'; @@ -17,7 +18,9 @@ const DesktopAppView = Loadable(lazy(() => import('./DesktopAppView'))); // Main App export default function MainView() { - const [allowMobile] = useLocalState((state) => [state.allowMobile]); + const [allowMobile] = useLocalState( + useShallow((state) => [state.allowMobile]) + ); // Set initial login status useEffect(() => { try { diff --git a/src/frontend/src/views/MobileAppView.tsx b/src/frontend/src/views/MobileAppView.tsx index 9b7ff00469..fa2c81f74e 100644 --- a/src/frontend/src/views/MobileAppView.tsx +++ b/src/frontend/src/views/MobileAppView.tsx @@ -1,13 +1,16 @@ import { Trans } from '@lingui/react/macro'; import { Anchor, Center, Container, Stack, Text, Title } from '@mantine/core'; +import { useShallow } from 'zustand/react/shallow'; import { ThemeContext } from '../contexts/ThemeContext'; import { docLinks } from '../defaults/links'; import { IS_DEV } from '../main'; import { useLocalState } from '../states/LocalState'; export default function MobileAppView() { - const [setAllowMobile] = useLocalState((state) => [state.setAllowMobile]); + const [setAllowMobile] = useLocalState( + useShallow((state) => [state.setAllowMobile]) + ); function ignore() { setAllowMobile(true); diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index e4fe880940..924b832068 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -5465,11 +5465,6 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9" - integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw== - util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -5712,9 +5707,7 @@ zod@^3.22.4: resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== -zustand@^4.5.5: - version "4.5.5" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1" - integrity sha512-+0PALYNJNgK6hldkgDq2vLrw5f6g/jCInz52n9RTpropGgeAf/ioFUCdtsjCqu4gNhW9D01rUQBROoRjdzyn2Q== - dependencies: - use-sync-external-store "1.2.2" +zustand@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.3.tgz#b323435b73d06b2512e93c77239634374b0e407f" + integrity sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==