elements
lifecycle
browser
- useAudio
- useBattery
- useBluetooth
- useBreakpoints
- useBroadcastChannel
- useBrowserLocation
- useClipboard
- useCopy
- useCssVar
- useDisplayMedia
- useDocumentEvent
- useDocumentTitle
- useDocumentVisibility
- useEventListener
- useEventSource
- useEyeDropper
- useFavicon
- useFileSystemAccess
- useFps
- useFullscreen
- useGamepad
- useGeolocation
- useMeasure
- useMediaControls
- useMediaQuery
- useMemory
- useNetwork
- useObjectUrl
- useOnline
- useOtpCredential
- usePermission
- usePictureInPicture
- usePointerLock
- usePostMessage
- useRaf
- useShare
- useSpeechRecognition
- useSpeechSynthesis
- useSticky
- useVibrate
- useVirtualKeyboard
- useWakeLock
- useWebSocket
utilities
state
- useBoolean
- useControllableState
- useCookie
- useCookies
- useCounter
- useDefault
- useDisclosure
- useField
- useHash
- useList
- useLocalStorage
- useMap
- useMask
- useMergedRef
- useObject
- useOffsetPagination
- useQueue
- useRafState
- useRefState
- useSessionStorage
- useSet
- useStateHistory
- useStep
- useStorage
- useToggle
- useUrlSearchParam
- useUrlSearchParams
- useWizard
user
sensors
- useDeviceMotion
- useDeviceOrientation
- useHotkeys
- useIdle
- useInfiniteScroll
- useIntersectionObserver
- useKeyboard
- useKeyPress
- useKeysPressed
- useMouse
- useMutationObserver
- useOrientation
- usePageLeave
- useParallax
- usePerformanceObserver
- useResizeObserver
- useScroll
- useScrollIntoView
- useScrollTo
- useSwipe
- useTextSelection
- useVisibility
- useWindowEvent
- useWindowFocus
- useWindowScroll
- useWindowSize
Account settings
Each field subscribes to its own slice — only the changed field re-renders.
import type { SubmitEvent } from 'react';
import { createStore, useDidUpdate } from '@siberiacancode/reactuse';
import { CheckIcon, ChevronDownIcon } from 'lucide-react';
import { memo, useRef } from 'react';
interface Profile {
bio: string;
email: string;
isPublic: boolean;
language: string;
name: string;
notifications: boolean;
}
const DEFAULT_PROFILE: Profile = {
name: 'siberiacancode',
email: 'hello@reactuse.org',
bio: 'Building open-source React hooks',
language: 'en',
notifications: true,
isPublic: false
};
const LANGUAGES = [
{ value: 'en', label: 'English' },
{ value: 'ru', label: 'Russian' },
{ value: 'de', label: 'German' }
];
const EMAIL_PATTERN = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/;
const profileStore = createStore(() => DEFAULT_PROFILE);
interface RerenderInfoProps {
componentName: string;
}
const RerenderInfo = ({ componentName }: RerenderInfoProps) => {
const countRef = useRef(0);
const badgeRef = useRef<HTMLSpanElement>(null);
useDidUpdate(() => {
countRef.current++;
if (badgeRef.current) {
badgeRef.current.textContent = `${componentName} x${countRef.current}`;
badgeRef.current.style.opacity = '1';
}
const timer = setTimeout(() => {
if (badgeRef.current) badgeRef.current.style.opacity = '0';
}, 1000);
return () => clearTimeout(timer);
});
return (
<span
ref={badgeRef}
className='bg-primary text-primary-foreground absolute -top-2.5 right-0 z-10 rounded-full px-2 py-0.5 font-mono text-[10px] font-semibold tabular-nums transition-opacity duration-300'
style={{ opacity: 0 }}
/>
);
};
const NameField = memo(() => {
const name = profileStore.use((state) => state.name);
const error = name.trim().length < 2 ? 'At least 2 characters' : '';
return (
<div className='relative flex flex-col gap-1.5'>
<RerenderInfo componentName='NameField' />
<label className='text-foreground text-xs font-medium' htmlFor='name'>
Display name
</label>
<input
className='border-border bg-card text-foreground rounded-md border px-3 py-2 text-sm outline-none'
id='name'
placeholder='Your name'
value={name}
onChange={(event) => profileStore.set({ name: event.target.value })}
/>
{error && <span className='text-destructive text-xs'>{error}</span>}
</div>
);
});
NameField.displayName = 'NameField';
const EmailField = memo(() => {
const email = profileStore.use((state) => state.email);
const error = !EMAIL_PATTERN.test(email) ? 'Invalid email format' : '';
return (
<div className='relative flex flex-col gap-1.5'>
<RerenderInfo componentName='EmailField' />
<label className='text-foreground text-xs font-medium' htmlFor='email'>
Email
</label>
<input
className='border-border bg-card text-foreground rounded-md border px-3 py-2 text-sm outline-none'
id='email'
placeholder='you@example.com'
type='email'
value={email}
onChange={(event) => profileStore.set({ email: event.target.value })}
/>
{error && <span className='text-destructive text-xs'>{error}</span>}
</div>
);
});
EmailField.displayName = 'EmailField';
const BioField = memo(() => {
const bio = profileStore.use((state) => state.bio);
return (
<div className='relative flex flex-col gap-1.5'>
<RerenderInfo componentName='BioField' />
<label className='text-foreground text-xs font-medium' htmlFor='bio'>
Bio
</label>
<textarea
className='border-border bg-card text-foreground min-h-[72px] resize-none rounded-md border px-3 py-2 text-sm outline-none'
id='bio'
placeholder='Tell something about yourself...'
value={bio}
onChange={(event) => profileStore.set({ bio: event.target.value })}
/>
</div>
);
});
BioField.displayName = 'BioField';
const NotificationsField = memo(() => {
const notifications = profileStore.use((state) => state.notifications);
return (
<label className='relative flex cursor-pointer items-start justify-between gap-3'>
<RerenderInfo componentName='NotificationsField' />
<div className='flex flex-col gap-0.5'>
<span className='text-foreground text-xs font-medium'>Email notifications</span>
<span className='text-muted-foreground text-[11px]'>
Receive product updates and release notes
</span>
</div>
<input
checked={notifications}
role='switch'
type='checkbox'
onChange={(event) => profileStore.set({ notifications: event.target.checked })}
/>
</label>
);
});
NotificationsField.displayName = 'NotificationsField';
const LanguageField = memo(() => {
const language = profileStore.use((state) => state.language);
return (
<div className='relative flex items-center justify-between gap-3'>
<RerenderInfo componentName='LanguageField' />
<div className='flex flex-col gap-0.5'>
<label className='text-foreground text-xs font-medium' htmlFor='language'>
Language
</label>
<span className='text-muted-foreground text-[11px]'>Choose your preferred language</span>
</div>
<div className='relative'>
<select
className='border-border bg-card text-foreground w-32 appearance-none rounded-md border py-1.5 pr-7 pl-3 text-xs outline-none'
id='language'
value={language}
onChange={(event) => profileStore.set({ language: event.target.value })}
>
{LANGUAGES.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
<ChevronDownIcon className='text-muted-foreground pointer-events-none absolute top-1/2 right-2 size-3.5 -translate-y-1/2' />
</div>
</div>
);
});
LanguageField.displayName = 'LanguageField';
const PublicField = memo(() => {
const isPublic = profileStore.use((state) => state.isPublic);
return (
<label className='relative flex cursor-pointer items-start gap-3'>
<RerenderInfo componentName='PublicField' />
<span className='mt-0.5 flex shrink-0 items-center'>
<input
checked={isPublic}
className='peer sr-only'
type='checkbox'
onChange={(event) => profileStore.set({ isPublic: event.target.checked })}
/>
<span className='border-border peer-checked:border-foreground peer-checked:bg-foreground flex size-4 items-center justify-center rounded-[5px] border transition-colors'>
{isPublic && <CheckIcon className='text-background size-3' strokeWidth={3.5} />}
</span>
</span>
<div className='flex flex-col gap-0.5'>
<span className='text-foreground text-xs font-medium'>Make profile public</span>
<span className='text-muted-foreground text-[11px]'>
Anyone on the internet can see your profile
</span>
</div>
</label>
);
});
PublicField.displayName = 'PublicField';
const Demo = () => {
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => event.preventDefault();
return (
<section className='flex w-full max-w-md flex-col gap-4 p-4'>
<div className='flex flex-col gap-1'>
<h2 className='text-foreground text-sm font-semibold'>Account settings</h2>
<p className='text-muted-foreground text-xs'>
Each field subscribes to its own slice — only the changed field re-renders.
</p>
</div>
<form className='flex flex-col gap-4' onSubmit={onSubmit}>
<NameField />
<EmailField />
<BioField />
<NotificationsField />
<LanguageField />
<PublicField />
</form>
</section>
);
};
export default Demo;
Installation
pnpm add @siberiacancode/reactuseUsage
const { set, get, use, subscribe } = createStore((set) => ({ count: 0, increment: () => set(state => ({ count: state.count + 1 })) }));Type Declarations
type StoreSetAction<Value> = ((prev: Value) => Partial<Value>) | Partial<Value>;
type StoreListener<Value> = (state: Value, prevState: Value) => void;
type StoreCreator<Value> = (
set: (action: StoreSetAction<Value>) => void,
get: () => Value
) => Value;
export interface StoreApi<Value> {
get: () => Value;
getInitial: () => Value;
set: (action: StoreSetAction<Value>) => void;
subscribe: (listener: StoreListener<Value>) => () => void;
use: (() => Value) &
(<Selected>(selector: (state: Value) => Selected) => Selected) &
(<Selected>(selector?: (state: Value) => Selected) => Selected | Value);
}API
Parameters
| Name | Type | Default | Note |
|---|---|---|---|
| createState | StateCreator<Value> | - | - Function that initializes the store state |
Returns
StoreApi<Value>Contributors
ddebabinyy0na24
Last updated on