@cyca/utils
v0.1.1
Published
Personal JS utils I use in the daily
Readme
@cyca/utils
A collection of utility functions and hooks for my TypeScript/JavaScript projects.
Installation
npm install @cyca/utils
# or
pnpm add @cyca/utils
# or
bun add @cyca/utilsTable of Contents
Core Utilities
invariant(condition, message)
Throws an error if the condition is falsey. Inspired by tiny-invariant.
import { invariant } from '@cyca/utils'
invariant(typeof value === 'string', 'value must be a string')Parameters:
condition: any- The condition to checkmessage: string | (() => string)- The message to throw (or a callback to generate the message)
Throws: InvariantError if condition is falsey
invariantResponse(condition, message, responseInit?)
Throws a 400 Response if the condition is falsey. Useful for server-side validation.
import { invariantResponse } from '@cyca/utils'
invariantResponse(typeof value === 'string', 'value must be a string')Parameters:
condition: any- The condition to checkmessage: string | (() => string)- The message to throwresponseInit?: ResponseInit- Additional response init options
Throws: Response if condition is falsey
isValidHttpUrl(string)
Checks if a string is a valid HTTP URL.
import { isValidHttpUrl } from '@cyca/utils'
if (isValidHttpUrl('https://example.com')) {
// valid URL
}delay(ms, value?)
Promise wrapper around setTimeout.
import { delay } from '@cyca/utils'
await delay(1000) // wait 1 second
const result = await delay(500, 'value') // wait 500ms and return 'value'Parameters:
ms: number- Milliseconds to waitvalue?: T- Optional value to resolve with
Returns: Promise<unknown>
prependHttp(url, options?)
Prepends http:// or https:// to a URL if it doesn't already have a protocol.
import { prependHttp } from '@cyca/utils'
prependHttp('example.com') // 'http://example.com'
prependHttp('example.com', { https: true }) // 'https://example.com'Parameters:
url: string- The URL to prependoptions?: { https?: boolean }- Options object
Returns: string
getUrlImageSize(url)
Gets the width and height of an image from a URL.
import { getUrlImageSize } from '@cyca/utils'
const { width, height } = await getUrlImageSize('https://example.com/image.jpg')Returns: Promise<{ width: number; height: number }>
toBase64(str)
Converts a string to base64.
import { toBase64 } from '@cyca/utils'
const encoded = toBase64('hello world')escapeRegExp(string)
Escapes special characters in a string for use in a regular expression.
import { escapeRegExp } from '@cyca/utils'
const escaped = escapeRegExp('hello.world')higlightWords(textToHighlight, searchTerm, options?)
Highlights words in text that match a search term.
import { higlightWords } from '@cyca/utils'
const result = higlightWords('Hello world', 'world', {
caseSensitive: false,
mark: (part, matched) => matched ? `<mark>${part}</mark>` : part
})Parameters:
textToHighlight: string- The text to search insearchTerm: string- The term to search foroptions?- Configuration objectcaseSensitive?: boolean- Whether to match casemark?: (part: string, matched: boolean) => T- Function to transform matched/unmatched partsappend?: (accum: T, part: P) => T | null- Function to accumulate results
debounce(func, timeout)
Creates a debounced function that delays invoking func until after timeout milliseconds.
import { debounce } from '@cyca/utils'
const debouncedFn = debounce(() => console.log('called'), 300)debounceAnimationFrame(func)
Creates a debounced function that uses requestAnimationFrame.
import { debounceAnimationFrame } from '@cyca/utils'
const debouncedFn = debounceAnimationFrame(() => console.log('called'))noop()
A no-op function that returns null.
import { noop } from '@cyca/utils'
const callback = noop()Deep Omit
deepOmit(object, path)
Deeply omits a property from an object using a path array.
import { deepOmit } from '@cyca/utils/deep-omit'
const obj = { a: { b: { c: 1, d: 2 } } }
const result = deepOmit(obj, ['a', 'b', 'c'])
// result: { a: { b: { d: 2 } } }Can also be used in curried form:
const omitPath = deepOmit(['a', 'b', 'c'])
const result = omitPath(obj)Type-safe: The path is type-checked against the object structure.
Jotai Utilities
useAtomCallback(callback, options?)
A custom version of Jotai's useAtomCallback that keeps the latest callback identity.
import { useAtomCallback } from '@cyca/utils/jotai'
const handleClick = useAtomCallback((get, set) => {
const value = get(someAtom)
set(anotherAtom, value + 1)
})Parameters:
callback: (get: Getter, set: Setter, ...args: Args) => Result- The callback functionoptions?: Options- Jotai store options
Returns: A memoized callback function
useSelectAtom(atom, selector)
Selects a derived value from an atom using a selector function.
import { useSelectAtom } from '@cyca/utils/jotai'
const userName = useSelectAtom(userAtom, (user) => user.name)Parameters:
atom: Atom<V>- The atom to read fromselector: (v: V) => S- Function to derive value
Returns: The selected value
withSnapshotHistory(targetAtom, limit?)
Wraps an atom with undo/redo history functionality.
import { withSnapshotHistory, COMMIT, UNDO, REDO, RESET } from '@cyca/utils/jotai'
const countAtom = atom(0)
const historyAtom = withSnapshotHistory(countAtom, 10)
// In a component:
const [{ value, previous, canUndo, canRedo }, dispatch] = useAtom(historyAtom)
// Update the value (doesn't create history entry)
dispatch(5)
// Commit to history
dispatch(COMMIT)
// Undo/redo
dispatch(UNDO)
dispatch(REDO)
// Reset history
dispatch(RESET)Parameters:
targetAtom: AnyWritableAtom<T>- The atom to tracklimit?: number- Maximum history entries (default: unlimited)
Actions:
COMMIT- Record current value to historyUNDO- Move back one stepREDO- Move forward one stepRESET- Clear all historySetStateAction<T>- Update value without creating history
init(callback, store?)
Initializes state or side effects when the store is created.
import { init } from '@cyca/utils/jotai'
const cleanup = init((get, set, store) => {
// Initialize something
set(someAtom, initialValue)
// Return cleanup function
return () => {
// Cleanup
}
})Parameters:
callback: (get: Getter, set: Setter, store: Store) => void | (() => void)- Initialization functionstore?: Store- Jotai store (uses default if not provided)
Returns: Cleanup function if provided by callback
Event Emitter (Mitt)
mitt(all?)
Creates a tiny event emitter.
import { mitt } from '@cyca/utils/mitt'
type Events = {
'user:login': { id: string }
'user:logout': void
}
const emitter = mitt<Events>()
emitter.on('user:login', (data) => {
console.log('User logged in:', data.id)
})
emitter.emit('user:login', { id: '123' })Parameters:
all?: EventHandlerMap<Events>- Optional initial handlers
Returns: Emitter<Events> with methods:
on(type, handler)- Subscribe to eventoff(type, handler)- Unsubscribe from eventemit(type, event)- Emit eventall- Map of all handlers
bind(emitter, event, listener)
Binds a listener to an event and returns an unbind function.
import { mitt, bind } from '@cyca/utils/mitt'
const emitter = mitt()
const unbind = bind(emitter, 'event', (data) => {
console.log(data)
})
// Later...
unbind()Returns: () => void - Unbind function
bindAll(emitter, listeners)
Binds multiple listeners at once and returns an unbind function.
import { mitt, bindAll } from '@cyca/utils/mitt'
type Events = {
'login': { id: string }
'logout': void
}
const emitter = mitt<Events>()
const unbind = bindAll(emitter, {
login: (data) => console.log('Login:', data.id),
logout: () => console.log('Logout')
})
// Later...
unbind()Returns: () => void - Unbind function that removes all listeners
React Hooks
useMediaQuery(query)
Hook to match media queries.
import { useMediaQuery } from '@cyca/utils/react'
const isMobile = useMediaQuery('(max-width: 768px)')Returns: boolean | undefined - undefined during SSR
useClientLayoutEffect
Use useEffect during SSR and useLayoutEffect in the browser to avoid warnings.
import { useClientLayoutEffect } from '@cyca/utils/react'
useClientLayoutEffect(() => {
// Safe to use in SSR
}, [])useConstant(input)
Creates a value exactly once. Unlike useMemo, this is guaranteed.
import { useConstant } from '@cyca/utils/react'
const expensiveObject = useConstant(() => createExpensiveObject())usePreviousEffect(effect, inputs)
Runs an effect with access to previous dependency values.
import { usePreviousEffect } from '@cyca/utils/react'
usePreviousEffect(([prevCount]) => {
console.log(`Count changed from ${prevCount} to ${count}`)
}, [count])useHasChangedEffect(effect, deps, equal?)
Runs an effect only when a dependency has changed.
import { useHasChangedEffect } from '@cyca/utils/react'
useHasChangedEffect(() => {
console.log('User changed')
}, [user], (a, b) => a.id === b.id)usePreviousValue(value)
Returns the previous value from the last render.
import { usePreviousValue } from '@cyca/utils/react'
const previousCount = usePreviousValue(count)useHasChanged(value)
Returns true if the value has changed since the last render.
import { useHasChanged } from '@cyca/utils/react'
const hasChanged = useHasChanged(count)useEvent(callback)
Creates an always-stable function identity. Easier to use than useCallback.
import { useEvent } from '@cyca/utils/react'
const handleClick = useEvent(() => {
console.log(count) // Always has latest value
})useLatest(value)
A React helper hook for storing latest value in ref object (updated in useLayoutEffect's callback). Similar to useEvent()
import { useLatest } from '@cyca/utils/react'
const valRef = useLatest(value)
// latest val
valRef.current
useAsyncCallback()
Returns a handler with pending states for async operations.
import { useAsyncCallback } from '@cyca/utils/react'
const [{ pending, success, error }, wrapAsync] = useAsyncCallback()
const handleSubmit = wrapAsync(async () => {
await api.submit()
})
if (pending) return <Spinner />
if (error) return <Error error={error} />Returns: [{ pending: boolean, success: boolean, error: Error | null }, wrap]
useEffectOnce(effectCb)
Runs an effect only once, even in strict mode.
import { useEffectOnce } from '@cyca/utils/react'
useEffectOnce(() => {
console.log('Runs only once')
})useDeepMemo(memoFn, key)
Memoizes a value using deep equality comparison.
import { useDeepMemo } from '@cyca/utils/react'
const memoizedValue = useDeepMemo(() => {
return computeExpensiveValue(obj)
}, [obj])useDeepMemoEffect(callback, deps)
Like useEffect but with deep equality comparison for dependencies.
import { useDeepMemoEffect } from '@cyca/utils/react'
useDeepMemoEffect(() => {
console.log('Object deeply changed')
}, [complexObject])useForceUpdate()
Returns a function to force a re-render.
import { useForceUpdate } from '@cyca/utils/react'
const [key, forceUpdate] = useForceUpdate()
// Later...
forceUpdate() // Forces re-renderuseIntersectionCallback(callback, options?)
Detects element visibility using Intersection Observer.
import { useIntersectionCallback } from '@cyca/utils/react'
const ref = useIntersectionCallback((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('Element is visible')
}
})
}, { threshold: 0.5 })
return <div ref={ref}>Content</div>useIsIntersecting(options?)
Returns a boolean indicating if element is intersecting and a ref callback.
import { useIsIntersecting } from '@cyca/utils/react'
const [isVisible, ref] = useIsIntersecting({ threshold: 0.5 })
return <div ref={ref}>{isVisible ? 'Visible' : 'Hidden'}</div>useIsVisible, useInView
Aliases for useIsIntersecting.
useFocusElementOnVisible()
Automatically focuses an element when it becomes visible.
import { useFocusElementOnVisible } from '@cyca/utils/react'
const ref = useFocusElementOnVisible()
return <input ref={ref} />useCounter(initialValue?)
A simple counter abstraction.
import { useCounter } from '@cyca/utils/react'
const { count, increment, decrement, reset, setCount } = useCounter(0)useSuspendAfterMount(callback)
Defers a suspense-triggering function until after mount.
import { useSuspendAfterMount } from '@cyca/utils/react'
const data = useSuspendAfterMount(() => fetchData())useLazyRef(fn)
A wrapper for useRef with lazy initialization.
import { useLazyRef } from '@cyca/utils/react'
const ref = useLazyRef(() => new ExpensiveClass())useInterval(callback, delay)
Runs a callback at a regular interval.
import { useInterval } from '@cyca/utils/react'
useInterval(() => {
console.log('Tick')
}, 1000)useAnimationFrame(cb, deps)
Runs a callback on every animation frame.
import { useAnimationFrame } from '@cyca/utils/react'
useAnimationFrame(({ time, delta }) => {
// Animation logic
}, [])useStyleMeasure(prefix?)
Dynamically measures element size and updates CSS custom properties.
import { useStyleMeasure } from '@cyca/utils/react'
const ref = useStyleMeasure('box')
// Sets --box-width and --box-height CSS variables
return <div ref={ref}>Content</div>withHook(useHook)
Converts a React hook into a render prop component.
import { withHook } from '@cyca/utils/react'
const WithCounter = withHook(useCounter)
<WithCounter args={[0]}>
{({ count, increment }) => (
<button onClick={increment}>{count}</button>
)}
</WithCounter>withSuspense(Component, Fallback?)
Wraps a component with Suspense boundary.
import { withSuspense } from '@cyca/utils/react'
const UserProfile = withSuspense(
UserProfileComponent,
() => <Spinner />
)License
MIT
