@fvc/hooks 1.1.1 → 1.1.2-next-ec65dfb844e6183b3d7f417eee613cfe5ecfd997
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -0
- package/package.json +14 -2
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# @fvc/hooks
|
|
2
|
+
|
|
3
|
+
`@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.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @fvc/hooks
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Peer Dependencies
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun add react antd
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Import
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { useId, useForceUpdate } from '@fvc/hooks';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { useId } from '@fvc/hooks';
|
|
27
|
+
|
|
28
|
+
export function TextField({ id, label }: { id?: string; label: string }) {
|
|
29
|
+
const fieldId = useId(id);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<label htmlFor={fieldId}>{label}</label>
|
|
34
|
+
<input id={fieldId} type="text" />
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Available Hooks
|
|
41
|
+
|
|
42
|
+
| Hook | Returns | Purpose |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| [`useId`](#useid) | `string` | Stable unique ID — caller-supplied or auto-generated |
|
|
45
|
+
| [`useForceUpdate`](#useforceupdate) | `() => void` | Unconditional re-render trigger for external state |
|
|
46
|
+
|
|
47
|
+
## useId
|
|
48
|
+
|
|
49
|
+
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.
|
|
50
|
+
|
|
51
|
+
> Distinct from React's built-in `useId` — this hook's primary value is the caller-override pattern. React's `useId` is SSR-safe but cannot be overridden by a prop.
|
|
52
|
+
|
|
53
|
+
### Parameters
|
|
54
|
+
|
|
55
|
+
| Parameter | Type | Default | Description |
|
|
56
|
+
| --- | --- | --- | --- |
|
|
57
|
+
| `id` | `string \| undefined` | `undefined` | When provided, returned as-is — skips generation entirely. |
|
|
58
|
+
| `generateId` | `() => string` | `randomId` | ID factory, called once on mount. Provide this for a custom naming scheme. |
|
|
59
|
+
|
|
60
|
+
### Common Usage
|
|
61
|
+
|
|
62
|
+
#### Auto-generated ID
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
function FormField({ label }: { label: string }) {
|
|
66
|
+
const fieldId = useId();
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<label htmlFor={fieldId}>{label}</label>
|
|
71
|
+
<input id={fieldId} />
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Caller-supplied ID
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
// Explicit id — required when a parent anchors aria-describedby or a test
|
|
81
|
+
// selector to a predictable value
|
|
82
|
+
<FormField id="signup-email" label="Email" />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Custom ID factory
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const prefixed = (prefix: string) => () =>
|
|
89
|
+
`${prefix}-${Math.random().toString(36).slice(2, 9)}`;
|
|
90
|
+
|
|
91
|
+
const fieldId = useId(undefined, prefixed('form'));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## useForceUpdate
|
|
95
|
+
|
|
96
|
+
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.
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
### Parameters
|
|
101
|
+
|
|
102
|
+
`useForceUpdate` takes no parameters.
|
|
103
|
+
|
|
104
|
+
| Returns | Type | Description |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `forceUpdate` | `() => void` | Calling it triggers an unconditional re-render. |
|
|
107
|
+
|
|
108
|
+
### Common Usage
|
|
109
|
+
|
|
110
|
+
#### Sync with an external interval
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
function LiveClock() {
|
|
114
|
+
const forceUpdate = useForceUpdate();
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const timer = setInterval(forceUpdate, 1000);
|
|
118
|
+
return () => clearInterval(timer);
|
|
119
|
+
}, [forceUpdate]);
|
|
120
|
+
|
|
121
|
+
return <time>{new Date().toLocaleTimeString()}</time>;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Sync with a mutable ref
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
function MutableCounter() {
|
|
129
|
+
const forceUpdate = useForceUpdate();
|
|
130
|
+
const count = useRef(0);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<button onClick={() => { count.current++; forceUpdate(); }}>
|
|
134
|
+
Count: {count.current}
|
|
135
|
+
</button>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Consumer Example
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
import { useId, useForceUpdate } from '@fvc/hooks';
|
|
144
|
+
import { useRef, useEffect } from 'react';
|
|
145
|
+
|
|
146
|
+
interface ProgressBarProps {
|
|
147
|
+
id?: string;
|
|
148
|
+
getProgress: () => number;
|
|
149
|
+
label: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function ProgressBar({ id, getProgress, label }: ProgressBarProps) {
|
|
153
|
+
const barId = useId(id);
|
|
154
|
+
const forceUpdate = useForceUpdate();
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const timer = setInterval(forceUpdate, 500);
|
|
158
|
+
return () => clearInterval(timer);
|
|
159
|
+
}, [forceUpdate]);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div>
|
|
163
|
+
<label htmlFor={barId}>{label}</label>
|
|
164
|
+
<progress id={barId} value={getProgress()} max={100} />
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Testing
|
|
171
|
+
|
|
172
|
+
Use `renderHook` from `@testing-library/react` to test hooks in isolation.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { renderHook, act } from '@testing-library/react';
|
|
176
|
+
import { useId, useForceUpdate } from '@fvc/hooks';
|
|
177
|
+
|
|
178
|
+
// useId
|
|
179
|
+
it('returns the provided id', () => {
|
|
180
|
+
const { result } = renderHook(() => useId('my-id'));
|
|
181
|
+
expect(result.current).toBe('my-id');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('generates a stable fvc-* id when no id is provided', () => {
|
|
185
|
+
const { result, rerender } = renderHook(() => useId());
|
|
186
|
+
const initial = result.current;
|
|
187
|
+
rerender();
|
|
188
|
+
expect(result.current).toBe(initial);
|
|
189
|
+
expect(result.current).toMatch(/^fvc-/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// useForceUpdate
|
|
193
|
+
it('triggers a re-render when called', () => {
|
|
194
|
+
let renderCount = 0;
|
|
195
|
+
const { result } = renderHook(() => {
|
|
196
|
+
renderCount++;
|
|
197
|
+
return useForceUpdate();
|
|
198
|
+
});
|
|
199
|
+
act(() => result.current());
|
|
200
|
+
expect(renderCount).toBe(2);
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## SSR Notes
|
|
205
|
+
|
|
206
|
+
Neither hook accesses `window`, `document`, or any browser API — both are safe in server-side environments.
|
|
207
|
+
|
|
208
|
+
`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.
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
bun run lint
|
|
214
|
+
bun run type-check
|
|
215
|
+
bun run test
|
|
216
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fvc/hooks",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2-next-ec65dfb844e6183b3d7f417eee613cfe5ecfd997",
|
|
4
4
|
"main": "./dist/lib/index.js",
|
|
5
5
|
"types": "./dist/lib/hooks/src/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -20,5 +20,17 @@
|
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"react": "^18.0.0",
|
|
22
22
|
"antd": "^5.0.0"
|
|
23
|
-
}
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"react",
|
|
26
|
+
"react-component",
|
|
27
|
+
"fvc",
|
|
28
|
+
"fe-vis-core",
|
|
29
|
+
"hooks",
|
|
30
|
+
"use-id",
|
|
31
|
+
"use-force-update",
|
|
32
|
+
"utilities",
|
|
33
|
+
"design-system",
|
|
34
|
+
"antd"
|
|
35
|
+
]
|
|
24
36
|
}
|