diff --git a/docs/src/components/header/ThemeModeToggle.tsx b/docs/src/components/header/ThemeModeToggle.tsx index 6a7667f796d8df..ff22e076ae3797 100644 --- a/docs/src/components/header/ThemeModeToggle.tsx +++ b/docs/src/components/header/ThemeModeToggle.tsx @@ -6,7 +6,6 @@ import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'; import LightModeOutlined from '@mui/icons-material/LightModeOutlined'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useChangeTheme } from 'docs/src/modules/components/ThemeContext'; -import useLocalStorageState from '@mui/utils/useLocalStorageState'; function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { const [mounted, setMounted] = React.useState(false); @@ -40,19 +39,30 @@ function CssVarsModeToggle(props: { onChange: (checked: boolean) => void }) { export default function ThemeModeToggle() { const theme = useTheme(); const changeTheme = useChangeTheme(); - const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); + const [mode, setMode] = React.useState(null); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const preferredMode = prefersDarkMode ? 'dark' : 'light'; + + React.useEffect(() => { + let initialMode = 'system'; + try { + initialMode = localStorage.getItem('mui-mode') || initialMode; + } catch (error) { + // do nothing + } + setMode(initialMode); + }, []); const handleChangeThemeMode = (checked: boolean) => { const paletteMode = checked ? 'dark' : 'light'; setMode(paletteMode); - }; - React.useEffect(() => { - const paletteMode = mode === 'system' ? preferredMode : mode; + try { + localStorage.setItem('mui-mode', paletteMode); // syncing with homepage, can be removed once all pages are migrated to CSS variables + } catch (error) { + // do nothing + } changeTheme({ paletteMode }); - }, [changeTheme, mode, preferredMode]); + }; if (mode === null) { return ; diff --git a/docs/src/modules/components/AppSettingsDrawer.js b/docs/src/modules/components/AppSettingsDrawer.js index 58d2a830cd787e..3ddad7cb06a71b 100644 --- a/docs/src/modules/components/AppSettingsDrawer.js +++ b/docs/src/modules/components/AppSettingsDrawer.js @@ -18,7 +18,6 @@ import FormatTextdirectionLToRIcon from '@mui/icons-material/FormatTextdirection import FormatTextdirectionRToLIcon from '@mui/icons-material/FormatTextdirectionRToL'; import { useChangeTheme } from 'docs/src/modules/components/ThemeContext'; import { useTranslate } from '@mui/docs/i18n'; -import useLocalStorageState from '@mui/utils/useLocalStorageState'; const Heading = styled(Typography)(({ theme }) => ({ margin: '20px 0 10px', @@ -43,22 +42,44 @@ function AppSettingsDrawer(props) { const t = useTranslate(); const upperTheme = useTheme(); const changeTheme = useChangeTheme(); - const [mode, setMode] = useLocalStorageState('mui-mode', 'system'); + const [mode, setMode] = React.useState(null); const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); const preferredMode = prefersDarkMode ? 'dark' : 'light'; + React.useEffect(() => { + // syncing with homepage, can be removed once all pages are migrated to CSS variables + let initialMode = 'system'; + try { + initialMode = localStorage.getItem('mui-mode') || initialMode; + } catch (error) { + // do nothing + } + setMode(initialMode); + }, [preferredMode]); + const handleChangeThemeMode = (event, paletteMode) => { if (paletteMode === null) { return; } setMode(paletteMode); - }; - React.useEffect(() => { - const paletteMode = mode === 'system' ? preferredMode : mode; - changeTheme({ paletteMode }); - }, [changeTheme, mode, preferredMode]); + if (paletteMode === 'system') { + try { + localStorage.setItem('mui-mode', 'system'); // syncing with homepage, can be removed once all pages are migrated to CSS variables + } catch (error) { + // thrown when cookies are disabled. + } + changeTheme({ paletteMode: preferredMode }); + } else { + try { + localStorage.setItem('mui-mode', paletteMode); // syncing with homepage, can be removed once all pages are migrated to CSS variables + } catch (error) { + // thrown when cookies are disabled. + } + changeTheme({ paletteMode }); + } + }; const handleChangeDirection = (event, direction) => { if (direction === null) { diff --git a/docs/src/modules/components/HighlightedCodeWithTabs.tsx b/docs/src/modules/components/HighlightedCodeWithTabs.tsx index bedd57ba5882f6..f31a70348e42df 100644 --- a/docs/src/modules/components/HighlightedCodeWithTabs.tsx +++ b/docs/src/modules/components/HighlightedCodeWithTabs.tsx @@ -4,7 +4,6 @@ import { Tabs, TabsOwnProps } from '@mui/base/Tabs'; import { TabsList as TabsListBase } from '@mui/base/TabsList'; import { TabPanel as TabPanelBase } from '@mui/base/TabPanel'; import { Tab as TabBase } from '@mui/base/Tab'; -import useLocalStorageState from '@mui/utils/useLocalStorageState'; import HighlightedCode from './HighlightedCode'; const TabList = styled(TabsListBase)(({ theme }) => ({ @@ -86,16 +85,36 @@ export default function HighlightedCodeWithTabs({ storageKey?: string; } & Record) { const availableTabs = React.useMemo(() => tabs.map(({ tab }) => tab), [tabs]); - const [activeTab, setActiveTab] = useLocalStorageState(storageKey ?? null, availableTabs[0]); + const [activeTab, setActiveTab] = React.useState(availableTabs[0]); const [mounted, setMounted] = React.useState(false); React.useEffect(() => { + try { + setActiveTab((prev) => { + if (storageKey === undefined) { + return prev; + } + const storedValues = localStorage.getItem(storageKey); + + return storedValues && availableTabs.includes(storedValues) ? storedValues : prev; + }); + } catch (error) { + // ignore error + } setMounted(true); - }, []); + }, [availableTabs, storageKey]); const handleChange: TabsOwnProps['onChange'] = (event, newValue) => { setActiveTab(newValue as string); + if (storageKey === undefined) { + return; + } + try { + localStorage.setItem(storageKey, newValue as string); + } catch (error) { + // ignore error + } }; const ownerState = { mounted }; diff --git a/packages/mui-utils/src/useLocalStorageState/index.ts b/packages/mui-utils/src/useLocalStorageState/index.ts deleted file mode 100644 index 33ff661f99ed20..00000000000000 --- a/packages/mui-utils/src/useLocalStorageState/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './useLocalStorageState'; diff --git a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts b/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts deleted file mode 100644 index d8ce3c4b836071..00000000000000 --- a/packages/mui-utils/src/useLocalStorageState/useLocalStorageState.ts +++ /dev/null @@ -1,155 +0,0 @@ -'use client'; - -import * as React from 'react'; - -const NOOP = () => {}; - -// storage events only work across tabs, we'll use an event emitter to announce within the current tab -const currentTabChangeListeners = new Map void>>(); - -function onCurrentTabStorageChange(key: string, handler: () => void) { - let listeners = currentTabChangeListeners.get(key); - - if (!listeners) { - listeners = new Set(); - currentTabChangeListeners.set(key, listeners); - } - - listeners.add(handler); -} - -function offCurrentTabStorageChange(key: string, handler: () => void) { - const listeners = currentTabChangeListeners.get(key); - if (!listeners) { - return; - } - - listeners.delete(handler); - - if (listeners.size === 0) { - currentTabChangeListeners.delete(key); - } -} - -function emitCurrentTabStorageChange(key: string) { - const listeners = currentTabChangeListeners.get(key); - if (listeners) { - listeners.forEach((listener) => listener()); - } -} - -function subscribe(area: Storage, key: string, cb: () => void): () => void { - const storageHandler = (event: StorageEvent) => { - if (event.storageArea === area && event.key === key) { - cb(); - } - }; - window.addEventListener('storage', storageHandler); - onCurrentTabStorageChange(key, cb); - return () => { - window.removeEventListener('storage', storageHandler); - offCurrentTabStorageChange(key, cb); - }; -} - -function getSnapshot(area: Storage, key: string): string | null { - return area.getItem(key); -} - -function setValue(area: Storage, key: string, value: string | null) { - if (typeof window !== 'undefined') { - if (value === null) { - area.removeItem(key); - } else { - area.setItem(key, String(value)); - } - emitCurrentTabStorageChange(key); - } -} - -type Initializer = () => T; - -type UseStorageStateHookResult = [T, React.Dispatch>]; - -function useLocalStorageStateServer( - key: string | null, - initializer: string | Initializer, -): UseStorageStateHookResult; -function useLocalStorageStateServer( - key: string | null, - initializer?: string | null | Initializer, -): UseStorageStateHookResult; -function useLocalStorageStateServer( - key: string | null, - initializer: string | null | Initializer = null, -): UseStorageStateHookResult | UseStorageStateHookResult { - const [initialValue] = React.useState(initializer); - return [initialValue, () => {}]; -} - -/** - * Sync state to local storage so that it persists through a page refresh. Usage is - * similar to useState except we pass in a storage key so that we can default - * to that value on page load instead of the specified initial value. - * - * Since the storage API isn't available in server-rendering environments, we - * return initialValue during SSR and hydration. - * - * Things this hook does different from existing solutions: - * - SSR-capable: it shows initial value during SSR and hydration, but immediately - * initializes when clientside mounted. - * - Sync state across tabs: When another tab changes the value in the storage area, the - * current tab follows suit. - */ -function useLocalStorageStateBrowser( - key: string | null, - initializer: string | Initializer, -): UseStorageStateHookResult; -function useLocalStorageStateBrowser( - key: string | null, - initializer?: string | null | Initializer, -): UseStorageStateHookResult; -function useLocalStorageStateBrowser( - key: string | null, - initializer: string | null | Initializer = null, -): UseStorageStateHookResult | UseStorageStateHookResult { - const [initialValue] = React.useState(initializer); - const area = window.localStorage; - const subscribeKey = React.useCallback( - (cb: () => void) => (key ? subscribe(area, key, cb) : NOOP), - [area, key], - ); - const getKeySnapshot = React.useCallback( - () => (key && getSnapshot(area, key)) ?? initialValue, - [area, initialValue, key], - ); - const getKeyServerSnapshot = React.useCallback(() => initialValue, [initialValue]); - - const storedValue = React.useSyncExternalStore( - subscribeKey, - getKeySnapshot, - getKeyServerSnapshot, - ); - - const setStoredValue = React.useCallback( - (value: React.SetStateAction) => { - if (key) { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setValue(area, key, valueToStore); - } - }, - [area, key, storedValue], - ); - - const [nonStoredValue, setNonStoredValue] = React.useState(initialValue); - - if (!key) { - return [nonStoredValue, setNonStoredValue]; - } - - return [storedValue, setStoredValue]; -} - -export default typeof window === 'undefined' - ? useLocalStorageStateServer - : useLocalStorageStateBrowser;