@fvc/hooks
v1.1.3
Published
`@fvc/hooks` provides shared React utility hooks for FE-VIS applications. Exposes two low-level primitives that recur across the component library: stable ID resolution for accessibility wiring, and an imperative re-render trigger for state that lives out
Readme
@fvc/hooks
@fvc/hooks provides shared React utility hooks for FE-VIS applications. Exposes two low-level primitives that recur across the component library: stable ID resolution for accessibility wiring, and an imperative re-render trigger for state that lives outside React.
Installation
bun add @fvc/hooksPeer Dependencies
bun add react antdImport
import { useId, useForceUpdate } from '@fvc/hooks';Quick Start
import { useId } from '@fvc/hooks';
export function TextField({ id, label }: { id?: string; label: string }) {
const fieldId = useId(id);
return (
<>
<label htmlFor={fieldId}>{label}</label>
<input id={fieldId} type="text" />
</>
);
}Available Hooks
| Hook | Returns | Purpose |
| --- | --- | --- |
| useId | string | Stable unique ID — caller-supplied or auto-generated |
| useForceUpdate | () => void | Unconditional re-render trigger for external state |
useId
A reusable component that renders a <label> and <input> pair needs an id that is stable across re-renders, unique per instance, and overridable by the caller. useId handles exactly that: it returns id unchanged when provided, and generates a fvc-{9 base36 chars} fallback on mount that never changes.
Distinct from React's built-in
useId— this hook's primary value is the caller-override pattern. React'suseIdis SSR-safe but cannot be overridden by a prop.
Parameters
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| id | string \| undefined | undefined | When provided, returned as-is — skips generation entirely. |
| generateId | () => string | randomId | ID factory, called once on mount. Provide this for a custom naming scheme. |
Common Usage
Auto-generated ID
function FormField({ label }: { label: string }) {
const fieldId = useId();
return (
<>
<label htmlFor={fieldId}>{label}</label>
<input id={fieldId} />
</>
);
}Caller-supplied ID
// Explicit id — required when a parent anchors aria-describedby or a test
// selector to a predictable value
<FormField id="signup-email" label="Email" />Custom ID factory
const prefixed = (prefix: string) => () =>
`${prefix}-${Math.random().toString(36).slice(2, 9)}`;
const fieldId = useId(undefined, prefixed('form'));useForceUpdate
Returns a stable dispatch function that triggers an unconditional re-render of the host component. Backed by useReducer, the returned reference never changes across renders — it is safe in useEffect dependency arrays and cleanup functions.
Use this only when state lives outside React's model — a useRef value, an external store subscription, or a mutable object from a third-party library — and the view needs to reflect a change React did not observe.
Parameters
useForceUpdate takes no parameters.
| Returns | Type | Description |
| --- | --- | --- |
| forceUpdate | () => void | Calling it triggers an unconditional re-render. |
Common Usage
Sync with an external interval
function LiveClock() {
const forceUpdate = useForceUpdate();
useEffect(() => {
const timer = setInterval(forceUpdate, 1000);
return () => clearInterval(timer);
}, [forceUpdate]);
return <time>{new Date().toLocaleTimeString()}</time>;
}Sync with a mutable ref
function MutableCounter() {
const forceUpdate = useForceUpdate();
const count = useRef(0);
return (
<button onClick={() => { count.current++; forceUpdate(); }}>
Count: {count.current}
</button>
);
}Consumer Example
import { useId, useForceUpdate } from '@fvc/hooks';
import { useRef, useEffect } from 'react';
interface ProgressBarProps {
id?: string;
getProgress: () => number;
label: string;
}
export function ProgressBar({ id, getProgress, label }: ProgressBarProps) {
const barId = useId(id);
const forceUpdate = useForceUpdate();
useEffect(() => {
const timer = setInterval(forceUpdate, 500);
return () => clearInterval(timer);
}, [forceUpdate]);
return (
<div>
<label htmlFor={barId}>{label}</label>
<progress id={barId} value={getProgress()} max={100} />
</div>
);
}Testing
Use renderHook from @testing-library/react to test hooks in isolation.
import { renderHook, act } from '@testing-library/react';
import { useId, useForceUpdate } from '@fvc/hooks';
// useId
it('returns the provided id', () => {
const { result } = renderHook(() => useId('my-id'));
expect(result.current).toBe('my-id');
});
it('generates a stable fvc-* id when no id is provided', () => {
const { result, rerender } = renderHook(() => useId());
const initial = result.current;
rerender();
expect(result.current).toBe(initial);
expect(result.current).toMatch(/@fvc-/);
});
// useForceUpdate
it('triggers a re-render when called', () => {
let renderCount = 0;
const { result } = renderHook(() => {
renderCount++;
return useForceUpdate();
});
act(() => result.current());
expect(renderCount).toBe(2);
});SSR Notes
Neither hook accesses window, document, or any browser API — both are safe in server-side environments.
useId generates its fallback ID with Math.random(). In SSR contexts, pass an explicit id prop to prevent hydration mismatches between server and client renders.
Development
bun run lint
bun run type-check
bun run test