@rocknblock/frontend-toolkit
v1.0.1
Published
Reusable React frontend hooks and utilities.
Downloads
186
Readme
@rocknblock/frontend-toolkit
Reusable React hooks and frontend utility functions.
Installation
npm i @rocknblock/frontend-toolkitPeer dependency:
react >= 18
Quick Start
import {
useCountdown,
useResource,
sleep,
mergeDeep,
createRouter,
type RouteParamValue,
} from '@rocknblock/frontend-toolkit';Exports
The package exports everything from src/hooks/* and src/utils/* via a single entrypoint.
Hooks
useAwaitinguseBeforeUnloaduseControlsuseCountdownuseDebounceCallbackuseDebouncedValueuseDeepEffectuseDeepMemouseFormattedTokenAmountuseIntervaluseLocalStorageuseMediauseMergeRefsuseMountedState,useMounted(alias)useResourceuseSetuseShallowSelectoruseValidateInputuseValueRef
Utils
getDevice,openWindowcamelize,decamelizeswap,toArray,hasIntersections,mapSet,removeBy,chunkCalls,paginatednoop,sleep,divide,capitalize,prepend,truncateHash,shortenPhraseextractErrorMsgnormalizeDecimalValue,validateDecimal,formatDate,numberFormatter,formatTokenAmount,formatThousands,percent,formatNumbercurry,retrypick,shallowEqual,isEmptyRecord,cloneDeep,mergeDeep,dataAttrreplaceParams,toParams,params,createRouter,validateQuery
Types
DeviceTypeRouteParamValuePaginationParamsValidateInputConfigValidateInputResultResourceStateUseResourceConfigUseResourceResult
Development
npm run typecheck
npm run lint
npm run buildOther scripts:
npm run formatnpm run lint:fixnpm run release
Build output: dist/.
Usage Examples
useResource: manual fetch + abort + optimistic local patch
import { useEffect, useState } from 'react';
import { useResource } from '@rocknblock/frontend-toolkit';
type User = { id: string; name: string; role: string };
export function UserPanel({ userId }: { userId: string }) {
const [enabled, setEnabled] = useState(false);
const { data, loading, error, refetch, setData, abort } = useResource<User, string>(userId, {
enabled,
immediate: false,
keepPreviousData: true,
abortable: true,
fetcher: async (id, signal) => {
const res = await fetch(`/api/users/${id}`, { signal });
if (!res.ok) throw new Error('Failed to load user');
return (await res.json()) as User;
},
onError: (e) => console.error(e),
});
useEffect(() => {
setEnabled(true);
void refetch();
return () => abort();
}, [refetch, abort]);
const promote = () => setData((prev) => (prev ? { ...prev, role: 'admin' } : prev));
if (loading) return <div>Loading...</div>;
if (error) return <div>Failed to load</div>;
return (
<div>
<div>{data?.name}</div>
<div>{data?.role}</div>
<button onClick={() => void refetch()}>Reload</button>
<button onClick={promote}>Promote locally</button>
</div>
);
}useShallowSelector: avoid unstable object identity in selected slice
import { useMemo } from 'react';
import { useShallowSelector } from '@rocknblock/frontend-toolkit';
type DashboardState = {
users: { online: number; total: number };
ui: { theme: string; sidebarOpen: boolean };
};
export function HeaderStats({ state }: { state: DashboardState }) {
const stats = useShallowSelector(state, (s) => ({
online: s.users.online,
total: s.users.total,
}));
const label = useMemo(() => `${stats.online}/${stats.total} online`, [stats]);
return <span>{label}</span>;
}useDeepEffect: run effect only on real deep changes in deps object
import { useState } from 'react';
import { useDeepEffect } from '@rocknblock/frontend-toolkit';
type Filters = {
tags: string[];
range: { from: string; to: string };
};
export function SearchPage() {
const [filters, setFilters] = useState<Filters>({
tags: ['react'],
range: { from: '2026-01-01', to: '2026-12-31' },
});
useDeepEffect(() => {
const query = new URLSearchParams({
tags: filters.tags.join(','),
from: filters.range.from,
to: filters.range.to,
});
void fetch(`/api/search?${query}`);
}, [filters]);
return <button onClick={() => setFilters((p) => ({ ...p }))}>Recreate same object</button>;
}useMergeRefs: combine forwarded ref with internal ref
import { forwardRef, useEffect, useRef } from 'react';
import { useMergeRefs } from '@rocknblock/frontend-toolkit';
type Props = { autoFocus?: boolean };
export const SearchInput = forwardRef<HTMLInputElement, Props>(function SearchInput(
{ autoFocus = false },
forwardedRef,
) {
const localRef = useRef<HTMLInputElement>(null);
const ref = useMergeRefs<HTMLInputElement>(forwardedRef, localRef);
useEffect(() => {
if (autoFocus) localRef.current?.focus();
}, [autoFocus]);
return <input ref={ref} placeholder="Search" />;
});useAwaiting: one loading flag for many concurrent async tasks
import { useAwaiting } from '@rocknblock/frontend-toolkit';
export function SaveButtons() {
const { loading, wrap } = useAwaiting();
const saveProfile = () =>
wrap(async () => {
await fetch('/api/profile', { method: 'POST' });
});
const saveSettings = () =>
wrap(async () => {
await fetch('/api/settings', { method: 'POST' });
});
return (
<div>
<button onClick={saveProfile} disabled={loading}>
Save profile
</button>
<button onClick={saveSettings} disabled={loading}>
Save settings
</button>
{loading && <span>Saving...</span>}
</div>
);
}useValidateInput: normalize + validate + explicit reset
import { useValidateInput } from '@rocknblock/frontend-toolkit';
export function AmountField() {
const amount = useValidateInput('', {
normalize: (v) => v.replace(',', '.').trim(),
validate: (v) => /^\d+(\.\d{1,2})?$/.test(v),
errorMessage: 'Enter a valid amount with up to 2 decimals',
});
return (
<div>
<input value={amount.value} onChange={(e) => amount.onChange(e.target.value)} />
{!amount.isValid && amount.touched && <div>{amount.error}</div>}
<button onClick={() => amount.reset('0')}>Reset to 0</button>
</div>
);
}createRouter + toParams
import { createRouter } from '@rocknblock/frontend-toolkit';
const router = createRouter('/app');
const url = router.query('/users/:id', { id: '42' }, { tab: 'profile', filter: ['a', 'b'] });
// /app/users/42?tab=profile&filter=a&filter=bmergeDeep
import { mergeDeep } from '@rocknblock/frontend-toolkit';
const config = mergeDeep({ api: { retries: 1, timeout: 1000 } }, { api: { timeout: 3000 } });
// { api: { retries: 1, timeout: 3000 } }