@slithy/utils
v0.3.0
Published
Shared utilities for @slithy packages.
Maintainers
Readme
@slithy/utils
Lightweight state management primitives and DOM utilities. Zero runtime dependencies — React is a peer dependency only.
Installation
npm install @slithy/utilsPeer dependencies: react@^17 || ^18 || ^19
bodyOverflowEffect
Locks body scroll when shouldLock is true and restores the original value on cleanup. Designed to be used as a useLayoutEffect callback.
import { bodyOverflowEffect } from '@slithy/utils'
useLayoutEffect(() => bodyOverflowEffect(modalsExist), [modalsExist])Returns a cleanup function when locking, void otherwise.
Cookies
deleteCookie
Deletes a cookie by name. Returns true if the cookie existed and was deleted, false if it did not exist.
import { deleteCookie } from '@slithy/utils'
deleteCookie('theme') // true if it existed, false otherwisegetCookie
Returns the value of a cookie by name, or null if it does not exist.
import { getCookie } from '@slithy/utils'
getCookie('theme') // "dark" | nullsetCookie
Sets a cookie with a name, value, and expiry in days.
import { setCookie } from '@slithy/utils'
setCookie('theme', 'dark', 7) // expires in 7 dayscreateStore
Creates a reactive store outside of React. Suitable for global or module-level state.
import { createStore } from '@slithy/utils'
const store = createStore<{ count: number }>(
() => ({ count: 0 }),
{ name: 'counter' } // optional — registers store for reset in tests
)
store.getState() // { count: 0 }
store.setState({ count: 1 })
store.setState(s => ({ count: s.count + 1 }))
const unsubscribe = store.subscribe(() => {
console.log(store.getState())
})
unsubscribe()
store.getInitialState() // { count: 0 }The subscriber receives no arguments — call store.getState() inside to read updated state.
createStoreContext
Builds a React context-backed store system for per-instance state (e.g. one store per component in a tree). Uses useSyncExternalStore internally.
import { createStoreContext } from '@slithy/utils'
type MyStore = { open: boolean; label: string }
const { storeContext, useStore, useState } = createStoreContext<MyStore>({
defaultState: () => ({ open: false, label: '' }),
name: 'MyComponent', // used in error messages
})
// Provider component
function MyProvider({ children, label }: { children: React.ReactNode; label: string }) {
const store = useStore(undefined, { label })
return <storeContext.Provider value={store}>{children}</storeContext.Provider>
}
// Consumer — selector form (returns selected value, re-renders only on change)
function MyConsumer() {
const open = useState(s => s.open)
return <div>{open ? 'Open' : 'Closed'}</div>
}
// Consumer — store form (returns full store object)
function MyOtherConsumer() {
const { getState, setState } = useState()
return <button onClick={() => setState({ open: !getState().open })}>Toggle</button>
}useState must be called within a component wrapped by the matching storeContext.Provider, or it will throw.
generateRandomId
import { generateRandomId } from '@slithy/utils'
generateRandomId() // "550e8400-e29b-41d4-a716-446655440000"
generateRandomId('layer') // "layer_550e8400-e29b-41d4-a716-446655440000"Uses crypto.randomUUID().
getFocusableElements
Returns all focusable elements within a container.
import { getFocusableElements } from '@slithy/utils'
const { firstElement, lastElement, elements } = getFocusableElements(containerElement)Finds: links, buttons, textareas, inputs (non-hidden), selects, elements with tabindex (not -1). Excludes disabled elements and elements with "hidden" in their class name.
storeResetRegistry
A global Set of named store reset functions. Useful in tests to reset all stores between cases.
import { storeResetRegistry } from '@slithy/utils'
afterEach(() => {
storeResetRegistry.forEach(({ reset }) => reset())
})Stores are registered automatically when createStore is called with a name option.
useClickGuard
Prevents accidental clicks on an element that appears in the same position as a closing element. Spread the returned props onto the guarded element.
import { useClickGuard } from '@slithy/utils'
function MyComponent() {
const clickGuardProps = useClickGuard()
return <div {...clickGuardProps}>{children}</div>
}Returns { onPointerDown, onClickCapture }. A click is only allowed through if it originated with a pointerdown on the guarded element itself.
useComponentMounted
Returns a ref whose .current is true after the component mounts and false after it unmounts. Useful for guarding async callbacks that should not run after the component is gone.
import { useComponentMounted } from '@slithy/utils'
function MyComponent() {
const isMounted = useComponentMounted()
useEffect(() => {
fetchData().then(data => {
if (isMounted.current) setState(data)
})
}, [])
}usePointerClick
Fires a handler on pointerup rather than click. Useful when a tap-and-drag gesture should still trigger the action, since dragging away cancels the click event but not the pointerup. Spread the returned props onto the element.
import { usePointerClick } from '@slithy/utils'
function CloseButton({ onClose }: { onClose: () => void }) {
const pointerClickProps = usePointerClick(onClose)
return <button {...pointerClickProps}>Close</button>
}Returns { onPointerDown, onPointerUp }. The handler only fires if pointerdown and pointerup both occurred on the same element.
usePrevious
Returns the value from the previous render. Returns undefined on the first render.
import { usePrevious } from '@slithy/utils'
function MyComponent({ isOpen }: { isOpen: boolean }) {
const wasOpen = usePrevious(isOpen)
// wasOpen is undefined on first render, then tracks the prior value
}useTrapFocus
Traps keyboard focus within a container. Spread the returned props onto the container element.
import { useTrapFocus } from '@slithy/utils'
function Modal() {
const trapFocusProps = useTrapFocus()
return <div {...trapFocusProps}>{children}</div>
}Pass false to disable trapping (returns {}):
const trapFocusProps = useTrapFocus(isOpen)Tab cycles forward through focusable elements; Shift+Tab cycles backward. Both wrap around at the boundaries.
