Skip to content

Commit 894d847

Browse files
committed
Refactor local storage
1 parent 7bca0e3 commit 894d847

File tree

7 files changed

+102
-114
lines changed

7 files changed

+102
-114
lines changed

packages/dev/s2-docs/src/BundlerSwitcher.tsx

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client';
22

33
import {Key} from 'react-aria-components';
4-
import React, {Children, ReactElement, ReactNode, useEffect, useMemo, useState} from 'react';
4+
import React, {Children, ReactElement, ReactNode, useMemo} from 'react';
55
import {SegmentedControl, SegmentedControlItem} from '@react-spectrum/s2';
66
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
7+
import {useLocalStorage} from './useLocalStorage';
78

89
type SwitcherKey = string;
910

@@ -42,7 +43,6 @@ export function BundlerSwitcherItem(_props: BundlerSwitcherItemProps) {
4243
}
4344

4445
export function BundlerSwitcher({children}: BundlerSwitcherProps) {
45-
const storageKey = 'bundler';
4646
let items = useMemo(() => {
4747
let arr = Children.toArray(children) as ReactElement<BundlerSwitcherItemProps>[];
4848
return arr
@@ -54,36 +54,14 @@ export function BundlerSwitcher({children}: BundlerSwitcherProps) {
5454
}));
5555
}, [children]);
5656

57-
let initial = useMemo(() => {
58-
let stored: string | null = null;
59-
if (storageKey && typeof window !== 'undefined') {
60-
stored = localStorage.getItem(storageKey);
61-
}
62-
let storedValid = items.find(it => it.id === stored)?.id;
63-
if (storedValid) {return storedValid;}
64-
return items[0]?.id;
65-
}, [items, storageKey]);
66-
67-
let [selected, setSelected] = useState<SwitcherKey | undefined>(initial);
68-
69-
useEffect(() => {
70-
// Update selected if items change and current is no longer valid
71-
if (selected && !items.find(it => it.id === selected)) {
72-
setSelected(items[0]?.id);
73-
}
74-
// eslint-disable-next-line react-hooks/exhaustive-deps
75-
}, [items]);
57+
let [bundler, setBundler] = useLocalStorage('bundler', items[0]?.id);
58+
let active = items.find(it => it.id === bundler) ?? items[0];
7659

7760
let onSelectionChange = (key: Key) => {
7861
let value = String(key) as SwitcherKey;
79-
setSelected(value);
80-
if (storageKey && typeof window !== 'undefined') {
81-
localStorage.setItem(storageKey, value);
82-
}
62+
setBundler(value);
8363
};
8464

85-
let active = items.find(it => it.id === selected) ?? items[0];
86-
8765
return (
8866
<div className={container}>
8967
<div className={style({overflowX: 'auto', width: 'auto', flexGrow: 1})}>

packages/dev/s2-docs/src/ExampleSwitcher.tsx

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {createContext, useState} from 'react';
55
import {Key} from 'react-aria-components';
66
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
77
import {useLayoutEffect} from '@react-aria/utils';
8+
import {useLocalStorage} from './useLocalStorage';
89

910
const exampleStyle = style({
1011
backgroundColor: 'layer-1',
@@ -59,66 +60,35 @@ const themePicker = style({
5960

6061
export const ExampleSwitcherContext = createContext<Key | null>(null);
6162

62-
const DEFAULT_EXAMPLES = ['Vanilla CSS', 'Tailwind'] as Key[];
63+
const DEFAULT_EXAMPLES = ['Vanilla CSS', 'Tailwind'];
6364

6465
export function ExampleSwitcher({type = 'style', examples = DEFAULT_EXAMPLES, children}) {
65-
let [selected, setSelected] = useState<Key>(examples[0]);
66-
let [theme, setTheme] = useState('indigo');
66+
let [selected, setSelected] = useLocalStorage(type, examples[0]);
67+
let [theme, setTheme] = useLocalStorage('theme', 'indigo');
68+
let [value, setValue] = useState(examples[0]);
6769

68-
useLayoutEffect(() => {
69-
if (!type) {
70-
return;
71-
}
72-
73-
let search = new URLSearchParams(location.search);
74-
let exampleType = search.get(type) ?? localStorage.getItem(type);
75-
if (exampleType && examples.includes(exampleType)) {
76-
setSelected(exampleType);
77-
}
78-
79-
let theme = localStorage.getItem('theme');
80-
if (theme) {
81-
setTheme(theme);
82-
}
83-
84-
let controller = new AbortController();
85-
window.addEventListener('storage', e => {
86-
if (e.key === type && e.newValue && examples.includes(e.newValue)) {
87-
setSelected(e.newValue);
88-
}
70+
if (!examples.includes(selected)) {
71+
selected = examples[0];
72+
}
8973

90-
if (e.key === 'theme' && e.newValue) {
91-
setTheme(e.newValue);
92-
}
93-
}, {signal: controller.signal});
94-
return () => controller.abort();
95-
}, [type, examples]);
74+
if (!type) {
75+
selected = value;
76+
}
9677

9778
useLayoutEffect(() => {
9879
document.documentElement.style.setProperty('--tint', `var(--${theme})`);
9980
}, [theme]);
10081

101-
let onSelectionChange = key => {
102-
setSelected(key);
103-
82+
let onSelectionChange = (key: Key) => {
10483
if (type) {
105-
localStorage.setItem(type, key);
106-
window.dispatchEvent(new StorageEvent('storage', {
107-
key: type,
108-
oldValue: String(selected),
109-
newValue: String(key)
110-
}));
84+
setSelected(String(key));
85+
} else {
86+
setValue(String(key));
11187
}
11288
};
11389

114-
let onThemeChange = key => {
115-
setTheme(key);
116-
localStorage.setItem('theme', key);
117-
window.dispatchEvent(new StorageEvent('storage', {
118-
key: 'theme',
119-
oldValue: String(theme),
120-
newValue: String(key)
121-
}));
90+
let onThemeChange = (key: Key | null) => {
91+
setTheme(String(key));
12292
};
12393

12494
return (

packages/dev/s2-docs/src/InstallCommand.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import {CopyButton} from './CopyButton';
44
import {iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'};
55
import {Key} from 'react-aria-components';
66
import Prompt from '@react-spectrum/s2/icons/Prompt';
7-
import React, {useEffect, useMemo, useState} from 'react';
7+
import React, {useMemo} from 'react';
88
import {SegmentedControl, SegmentedControlItem} from '@react-spectrum/s2';
9+
import {useLocalStorage} from './useLocalStorage';
910

1011
const container = style({
1112
backgroundColor: 'layer-1',
@@ -56,22 +57,11 @@ export interface InstallCommandProps {
5657
}
5758

5859
export function InstallCommand({pkg, flags, label, isCommand}: InstallCommandProps) {
59-
let [manager, setManager] = useState<PkgManager>('yarn');
60-
61-
useEffect(() => {
62-
if (isCommand) {
63-
return;
64-
}
65-
let stored = localStorage.getItem('packageManager');
66-
if (stored === 'npm' || stored === 'pnpm' || stored === 'yarn') {
67-
setManager(stored);
68-
}
69-
}, [isCommand]);
60+
let [manager, setManager] = useLocalStorage('packageManager', 'npm');
7061

7162
let onSelectionChange = (key: Key) => {
7263
let value = String(key) as PkgManager;
7364
setManager(value);
74-
localStorage.setItem('packageManager', value);
7565
};
7666

7767
let command = useMemo(() => {
@@ -102,8 +92,8 @@ export function InstallCommand({pkg, flags, label, isCommand}: InstallCommandPro
10292
<div className={container} data-example-switcher>
10393
{!isCommand && (
10494
<SegmentedControl selectedKey={manager} onSelectionChange={onSelectionChange} styles={switcher}>
105-
<SegmentedControlItem id="yarn">yarn</SegmentedControlItem>
10695
<SegmentedControlItem id="npm">npm</SegmentedControlItem>
96+
<SegmentedControlItem id="yarn">yarn</SegmentedControlItem>
10797
<SegmentedControlItem id="pnpm">pnpm</SegmentedControlItem>
10898
</SegmentedControl>
10999
)}

packages/dev/s2-docs/src/ShadcnCommand.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,20 @@ import CopyButton from './CopyButton';
33
import {iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'};
44
import {Key, SegmentedControl, SegmentedControlItem} from '@react-spectrum/s2';
55
import Prompt from '@react-spectrum/s2/icons/Prompt';
6-
import {RefObject, useEffect, useState} from 'react';
6+
import {RefObject} from 'react';
7+
import {useLocalStorage} from './useLocalStorage';
78

89
export function ShadcnCommand({registryUrl, preRef}: {registryUrl: string, preRef?: RefObject<HTMLPreElement | null>}) {
9-
let [packageManager, setPackageManager] = useState<Key>('npm');
10+
let [packageManager, setPackageManager] = useLocalStorage('packageManager', 'npm');
1011
let command = packageManager;
1112
if (packageManager === 'npm') {
1213
command = 'npx';
1314
} else if (packageManager === 'pnpm') {
1415
command = 'pnpm dlx';
1516
}
1617

17-
useEffect(() => {
18-
let value = localStorage.getItem('packageManager');
19-
if (value) {
20-
setPackageManager(value);
21-
}
22-
}, []);
23-
2418
let onSelectionChange = (value: Key) => {
25-
setPackageManager(value);
26-
localStorage.setItem('packageManager', String(value));
19+
setPackageManager(String(value));
2720
};
2821

2922
let cmd = `${command} shadcn@latest add ${process.env.REGISTRY_URL || 'http://localhost:8081'}/${registryUrl}`;

packages/dev/s2-docs/src/StarterKits.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const preview = style({
1616

1717
export function StarterKits() {
1818
return (
19-
<section className={style({display: 'flex', columnGap: 16, flexWrap: 'wrap'})}>
19+
<section className={style({display: 'flex', gap: 16, flexWrap: 'wrap'})}>
2020
<div className={style({display: 'flex', flexDirection: 'column', gap: 8})}>
2121
<Card href={`../react-aria-starter.${gitHash}.zip`}>
2222
<div className={style({display: 'flex', alignItems: 'center'})}>

packages/dev/s2-docs/src/Step.tsx

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,6 @@ export function Step({children}) {
2424
listStyleType: 'none',
2525
position: 'relative'
2626
})}>
27-
{/* <div
28-
className={style({
29-
display: {
30-
default: 'block',
31-
':is(:last-child > *)': 'none'
32-
},
33-
position: 'absolute',
34-
top: 'calc(1lh + 16px)',
35-
left: 'calc(-1lh / 2 - 8px)',
36-
bottom: -24,
37-
width: 2,
38-
borderRadius: 'full',
39-
backgroundColor: 'gray-400'
40-
})} /> */}
4127
{children}
4228
</li>
4329
);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import {useState, useSyncExternalStore} from 'react';
2+
3+
export function useLocalStorage(key: string, defaultValue: string): [string, (value: string) => void] {
4+
let [store] = useState(() => new Store(key, defaultValue));
5+
let value = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
6+
return [value, store.setValue];
7+
}
8+
9+
class Store {
10+
key: string;
11+
defaultValue: string;
12+
subscriptions: Set<() => void>;
13+
14+
constructor(key: string, defaultValue: string) {
15+
this.key = key;
16+
this.defaultValue = defaultValue;
17+
this.subscriptions = new Set();
18+
}
19+
20+
subscribe = (fn: () => void) => {
21+
if (!this.key) {
22+
return () => {};
23+
}
24+
25+
let onStorage = (e: StorageEvent) => {
26+
if (e.key === this.key) {
27+
fn();
28+
}
29+
};
30+
31+
window.addEventListener('storage', onStorage);
32+
return () => {
33+
window.removeEventListener('storage', onStorage);
34+
};
35+
};
36+
37+
getSnapshot = () => {
38+
if (!this.key) {
39+
return this.defaultValue;
40+
}
41+
42+
let search = new URLSearchParams(location.search);
43+
return search.get(this.key) ?? localStorage.getItem(this.key) ?? this.defaultValue;
44+
};
45+
46+
getServerSnapshot = () => {
47+
return this.defaultValue;
48+
};
49+
50+
setValue = (value: string) => {
51+
if (!this.key) {
52+
return;
53+
}
54+
55+
let oldValue = this.getSnapshot();
56+
localStorage.setItem(this.key, value);
57+
58+
let search = new URLSearchParams(location.search);
59+
if (search.has(this.key)) {
60+
search.set(this.key, value);
61+
let url = new URL('?' + search.toString(), location.href);
62+
history.replaceState(null, '', url.toString());
63+
}
64+
65+
window.dispatchEvent(new StorageEvent('storage', {
66+
key: this.key,
67+
oldValue,
68+
newValue: value
69+
}));
70+
};
71+
}

0 commit comments

Comments
 (0)