aidos-ui 3.0.31 → 4.0.1
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/.claude/settings.local.json +14 -1
- package/CLAUDE.md +72 -0
- package/README.md +43 -29
- package/dist/Badge.d.ts +2 -2
- package/dist/Badge.js.map +1 -1
- package/dist/BaseButton.d.ts +2 -2
- package/dist/BaseButton.js +2 -2
- package/dist/BaseButton.js.map +1 -1
- package/dist/BaseInput.d.ts +2 -2
- package/dist/BaseInput.js.map +1 -1
- package/dist/BaseLink.d.ts +2 -2
- package/dist/BaseLink.js +2 -2
- package/dist/BaseLink.js.map +1 -1
- package/dist/BaseListItem.d.ts +3 -3
- package/dist/BaseListItem.js.map +1 -1
- package/dist/BaseView.d.ts +2 -2
- package/dist/BaseView.js.map +1 -1
- package/dist/Calendar.d.ts +4 -4
- package/dist/DarkMode.js.map +1 -1
- package/dist/DateInput.d.ts +2 -2
- package/dist/DatePicker.js.map +1 -1
- package/dist/DatetimeInput.d.ts +2 -2
- package/dist/Dialog.d.ts +3 -3
- package/dist/Dialog.js.map +1 -1
- package/dist/Dropdown.d.ts +3 -3
- package/dist/Dropdown.js.map +1 -1
- package/dist/Icon.d.ts +3 -3
- package/dist/Icon.js +2 -21
- package/dist/Icon.js.map +1 -1
- package/dist/IconLink.js +2 -1
- package/dist/IconLink.js.map +1 -1
- package/dist/Interactable.d.ts +4 -4
- package/dist/Interactable.js +2 -2
- package/dist/Interactable.js.map +1 -1
- package/dist/ListButtonItem.js +2 -2
- package/dist/ListButtonItem.js.map +1 -1
- package/dist/ListLinkItem.js +2 -2
- package/dist/ListLinkItem.js.map +1 -1
- package/dist/MenuIcon.js.map +1 -1
- package/dist/NextJSRouter.js.map +1 -1
- package/dist/NextJSRouterState.js.map +1 -1
- package/dist/OnVisible.d.ts +1 -1
- package/dist/OnVisible.js.map +1 -1
- package/dist/Popover.d.ts +7 -7
- package/dist/Popover.js +2 -2
- package/dist/Popover.js.map +1 -1
- package/dist/ProgressBar.d.ts +2 -2
- package/dist/ProgressBar.js.map +1 -1
- package/dist/Providers.d.ts +2 -2
- package/dist/Providers.js +2 -2
- package/dist/Providers.js.map +1 -1
- package/dist/RangeInput.d.ts +3 -3
- package/dist/RangeInput.js +2 -2
- package/dist/RangeInput.js.map +1 -1
- package/dist/RootView.js.map +1 -1
- package/dist/Select.d.ts +2 -3
- package/dist/Select.js.map +1 -1
- package/dist/StaticCheckbox.js.map +1 -1
- package/dist/Storage.d.ts +2 -1
- package/dist/Storage.js.map +1 -1
- package/dist/Stream.js.map +1 -1
- package/dist/Sublist.d.ts +2 -2
- package/dist/Tabs.d.ts +4 -4
- package/dist/Tabs.js +2 -4
- package/dist/Tabs.js.map +1 -1
- package/dist/Text.d.ts +2 -2
- package/dist/Text.js.map +1 -1
- package/dist/TextArea.d.ts +2 -2
- package/dist/TextArea.js.map +1 -1
- package/dist/TextInput.d.ts +4 -3
- package/dist/TextInput.js +14 -5
- package/dist/TextInput.js.map +1 -1
- package/dist/TextPairing.d.ts +2 -2
- package/dist/TextPairing.js.map +1 -1
- package/dist/Tooltip.d.ts +4 -4
- package/dist/Tooltip.js.map +1 -1
- package/dist/aria.js.map +1 -1
- package/dist/hash.js.map +1 -1
- package/dist/jss.d.ts +21 -21
- package/dist/jss.js +6 -6
- package/dist/jss.js.map +1 -1
- package/dist/numberToBase.js.map +1 -1
- package/dist/stateMachineContextFactory.js.map +1 -1
- package/dist/useCookie.js.map +1 -1
- package/dist/useKeyboard.js.map +1 -1
- package/dist/useNavigation.js.map +1 -1
- package/dist/useRefEffect.js.map +1 -1
- package/next-env.d.ts +2 -1
- package/next.config.js +2 -1
- package/package.json +27 -20
- package/src/Badge.tsx +2 -2
- package/src/BaseButton.tsx +4 -4
- package/src/BaseInput.tsx +2 -2
- package/src/BaseLink.tsx +4 -4
- package/src/BaseListItem.tsx +3 -3
- package/src/BaseView.tsx +3 -3
- package/src/Calendar.tsx +4 -4
- package/src/DateInput.tsx +2 -2
- package/src/DatetimeInput.tsx +2 -2
- package/src/Dialog.tsx +3 -3
- package/src/Dropdown.tsx +4 -4
- package/src/Icon.tsx +4 -30
- package/src/IconLink.tsx +2 -1
- package/src/Interactable.tsx +4 -4
- package/src/ListButtonItem.tsx +2 -2
- package/src/ListLinkItem.tsx +2 -2
- package/src/MenuIcon.tsx +2 -2
- package/src/NextJSRouter.tsx +2 -2
- package/src/Popover.tsx +9 -9
- package/src/ProgressBar.tsx +3 -3
- package/src/Providers.tsx +4 -4
- package/src/RangeInput.tsx +5 -5
- package/src/Select.tsx +2 -2
- package/src/StaticCheckbox.tsx +2 -2
- package/src/Storage.tsx +1 -1
- package/src/Sublist.tsx +2 -2
- package/src/Tabs.tsx +4 -6
- package/src/Text.tsx +2 -2
- package/src/TextArea.tsx +2 -2
- package/src/TextInput.tsx +20 -8
- package/src/TextPairing.tsx +2 -2
- package/src/Tooltip.tsx +5 -5
- package/src/docs/mdx.tsx +40 -0
- package/src/docs/searchIndex.ts +1 -1
- package/src/jss.tsx +18 -18
- package/src/pages/Badge.mdx +136 -14
- package/src/pages/Button.mdx +121 -63
- package/src/pages/Calendar.mdx +196 -2
- package/src/pages/Card.mdx +123 -47
- package/src/pages/Checkbox.mdx +103 -28
- package/src/pages/Dialog.mdx +172 -7
- package/src/pages/FlexLayout.mdx +185 -51
- package/src/pages/Hashing.mdx +199 -8
- package/src/pages/Icon.mdx +150 -42
- package/src/pages/IconButton.mdx +127 -116
- package/src/pages/Link.mdx +118 -47
- package/src/pages/List.mdx +167 -283
- package/src/pages/ListHeader.mdx +157 -27
- package/src/pages/Popover.mdx +177 -55
- package/src/pages/ProgressBar.mdx +186 -6
- package/src/pages/ProgressCircle.mdx +196 -13
- package/src/pages/RangeInput.mdx +189 -61
- package/src/pages/Tabs.mdx +202 -346
- package/src/pages/Text.mdx +102 -81
- package/src/pages/TextInput.mdx +169 -44
- package/src/pages/TextPairing.mdx +164 -115
- package/src/pages/TimeInput.mdx +142 -37
- package/src/pages/Tooltip.mdx +99 -12
- package/src/pages/_app.tsx +14 -4
- package/src/pages/css-in-js.mdx +196 -83
- package/src/pages/navigation.mdx +122 -20
- package/src/pages/useCookie.mdx +168 -7
- package/src/pages/useKeyboard.mdx +242 -5
- package/src/pages/useNavigation.mdx +244 -16
- package/src/pages/usePromise.mdx +271 -6
- package/src/pages/useRefEffect.mdx +230 -28
- package/tsconfig.json +13 -4
package/src/pages/usePromise.mdx
CHANGED
|
@@ -1,15 +1,280 @@
|
|
|
1
1
|
# usePromise
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A React hook for unwrapping Promise values into React state. Handles loading, success, and error states automatically.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Signature
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
function usePromise<T>(
|
|
11
|
+
promiseFactory: () => Promise<T>,
|
|
12
|
+
inputs: Array<any>,
|
|
13
|
+
initialValue: T
|
|
14
|
+
): [T | null, any]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Parameters
|
|
20
|
+
|
|
21
|
+
| Parameter | Type | Description |
|
|
22
|
+
|-----------|------|-------------|
|
|
23
|
+
| `promiseFactory` | `() => Promise<T>` | Factory function that returns a Promise |
|
|
24
|
+
| `inputs` | `Array<any>` | Dependency array (like useEffect deps) |
|
|
25
|
+
| `initialValue` | `T` | Value to use before the Promise resolves |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Returns
|
|
30
|
+
|
|
31
|
+
A tuple with two elements:
|
|
32
|
+
|
|
33
|
+
| Index | Type | Description |
|
|
34
|
+
|-------|------|-------------|
|
|
35
|
+
| `0` | `T \| null` | The resolved value, or `initialValue` while loading |
|
|
36
|
+
| `1` | `any` | Error if the Promise rejected, otherwise `null` |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Basic usage
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { usePromise } from "aidos-ui";
|
|
44
|
+
|
|
45
|
+
function UserProfile({ userId }) {
|
|
46
|
+
const [user, error] = usePromise(
|
|
47
|
+
() => fetchUser(userId),
|
|
48
|
+
[userId],
|
|
49
|
+
null
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (error) {
|
|
53
|
+
return <ErrorMessage error={error} />;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!user) {
|
|
57
|
+
return <LoadingSpinner />;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return <div>{user.name}</div>;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## With initial value
|
|
67
|
+
|
|
68
|
+
Provide an initial value to show immediately while loading:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
function Counter() {
|
|
72
|
+
const [count, error] = usePromise(
|
|
73
|
+
() => fetchCurrentCount(),
|
|
74
|
+
[],
|
|
75
|
+
0 // Show 0 while loading
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return <span>{count}</span>;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Dependency-based refetching
|
|
85
|
+
|
|
86
|
+
The Promise re-executes when dependencies change:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
function SearchResults({ query }) {
|
|
90
|
+
const [results, error] = usePromise(
|
|
91
|
+
() => searchAPI(query),
|
|
92
|
+
[query], // Refetch when query changes
|
|
93
|
+
[]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<ul>
|
|
98
|
+
{results.map((result) => (
|
|
99
|
+
<li key={result.id}>{result.title}</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Error handling
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
function DataDisplay() {
|
|
112
|
+
const [data, error] = usePromise(
|
|
113
|
+
() => fetchData(),
|
|
114
|
+
[],
|
|
115
|
+
null
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (error) {
|
|
119
|
+
return (
|
|
120
|
+
<Card>
|
|
121
|
+
<Span color="negative">Error: {error.message}</Span>
|
|
122
|
+
<Button onClick={() => window.location.reload()}>
|
|
123
|
+
Retry
|
|
124
|
+
</Button>
|
|
125
|
+
</Card>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!data) {
|
|
130
|
+
return <Spinner />;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return <DataTable data={data} />;
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Multiple async calls
|
|
140
|
+
|
|
141
|
+
Use multiple `usePromise` hooks for parallel data fetching:
|
|
4
142
|
|
|
5
143
|
```js
|
|
6
|
-
function
|
|
7
|
-
const [
|
|
144
|
+
function Dashboard() {
|
|
145
|
+
const [stats, statsError] = usePromise(
|
|
146
|
+
() => fetchStats(),
|
|
147
|
+
[],
|
|
148
|
+
null
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const [notifications, notifError] = usePromise(
|
|
152
|
+
() => fetchNotifications(),
|
|
153
|
+
[],
|
|
154
|
+
[]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const [user, userError] = usePromise(
|
|
158
|
+
() => fetchCurrentUser(),
|
|
159
|
+
[],
|
|
160
|
+
null
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const isLoading = !stats || !user;
|
|
164
|
+
const hasError = statsError || notifError || userError;
|
|
165
|
+
|
|
166
|
+
if (hasError) {
|
|
167
|
+
return <ErrorPage />;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (isLoading) {
|
|
171
|
+
return <LoadingSkeleton />;
|
|
172
|
+
}
|
|
8
173
|
|
|
9
|
-
|
|
10
|
-
|
|
174
|
+
return (
|
|
175
|
+
<Column gap="large">
|
|
176
|
+
<StatsWidget stats={stats} />
|
|
177
|
+
<NotificationList notifications={notifications} />
|
|
178
|
+
<UserGreeting user={user} />
|
|
179
|
+
</Column>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## With typed responses
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
interface User {
|
|
190
|
+
id: string;
|
|
191
|
+
name: string;
|
|
192
|
+
email: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function UserCard({ userId }: { userId: string }) {
|
|
196
|
+
const [user, error] = usePromise<User | null>(
|
|
197
|
+
() => fetchUser(userId),
|
|
198
|
+
[userId],
|
|
199
|
+
null
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// user is typed as User | null
|
|
203
|
+
if (user) {
|
|
204
|
+
return <span>{user.name}</span>;
|
|
11
205
|
}
|
|
12
206
|
|
|
13
|
-
return <span>
|
|
207
|
+
return <span>Loading...</span>;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Conditional fetching
|
|
214
|
+
|
|
215
|
+
Use dependencies to control when fetching occurs:
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
function OptionalData({ shouldFetch, id }) {
|
|
219
|
+
const [data, error] = usePromise(
|
|
220
|
+
() => shouldFetch ? fetchData(id) : Promise.resolve(null),
|
|
221
|
+
[shouldFetch, id],
|
|
222
|
+
null
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// ...
|
|
14
226
|
}
|
|
15
227
|
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Comparison with other approaches
|
|
232
|
+
|
|
233
|
+
### vs. plain useEffect
|
|
234
|
+
|
|
235
|
+
```js
|
|
236
|
+
// ❌ Manual state management
|
|
237
|
+
const [data, setData] = useState(null);
|
|
238
|
+
const [error, setError] = useState(null);
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
fetchData()
|
|
242
|
+
.then(setData)
|
|
243
|
+
.catch(setError);
|
|
244
|
+
}, []);
|
|
245
|
+
|
|
246
|
+
// ✅ usePromise - cleaner
|
|
247
|
+
const [data, error] = usePromise(() => fetchData(), [], null);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### vs. React Query / SWR
|
|
251
|
+
|
|
252
|
+
`usePromise` is simpler but lacks:
|
|
253
|
+
- Caching
|
|
254
|
+
- Background refetching
|
|
255
|
+
- Stale-while-revalidate
|
|
256
|
+
- Automatic retries
|
|
257
|
+
|
|
258
|
+
Use `usePromise` for simple cases; consider React Query or SWR for complex data fetching needs.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Best practices
|
|
263
|
+
|
|
264
|
+
1. **Use meaningful initial values** - Choose values that make sense for your UI (empty array for lists, null for objects)
|
|
265
|
+
|
|
266
|
+
2. **Handle all states** - Always check for error and loading states
|
|
267
|
+
|
|
268
|
+
3. **Keep factories stable** - Avoid creating new functions on each render:
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
// ❌ Creates new function each render
|
|
272
|
+
const [data] = usePromise(() => fetch(`/api/${id}`), [id], null);
|
|
273
|
+
|
|
274
|
+
// ✅ Factory only changes when id changes
|
|
275
|
+
const [data] = usePromise(
|
|
276
|
+
() => fetchById(id),
|
|
277
|
+
[id],
|
|
278
|
+
null
|
|
279
|
+
);
|
|
280
|
+
```
|
|
@@ -1,42 +1,192 @@
|
|
|
1
1
|
# useRefEffect
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A React hook that reliably runs an effect once when an element is mounted. Unlike `useEffect`, this hook guarantees single execution and proper cleanup, making it ideal for setting up event listeners and DOM manipulations.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Signature
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
function useRefEffect<T>(
|
|
11
|
+
callback: (root: T) => (() => void) | void
|
|
12
|
+
): (root: T) => void
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Parameters
|
|
18
|
+
|
|
19
|
+
| Parameter | Type | Description |
|
|
20
|
+
|-----------|------|-------------|
|
|
21
|
+
| `callback` | `(root: T) => (() => void) \| void` | Function called when the element mounts. Can return a cleanup function. |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Returns
|
|
26
|
+
|
|
27
|
+
A ref callback function that should be passed to the element's `ref` prop.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Why use this?
|
|
32
|
+
|
|
33
|
+
React's `useEffect` with an empty dependency array doesn't reliably run only once due to:
|
|
34
|
+
- Strict Mode double-invocation in development
|
|
35
|
+
- Concurrent rendering behaviors
|
|
36
|
+
- Fast refresh during development
|
|
37
|
+
|
|
38
|
+
`useRefEffect` solves this by using a ref callback pattern that guarantees:
|
|
39
|
+
- The effect runs exactly once when the element mounts
|
|
40
|
+
- Cleanup runs exactly once when the element unmounts
|
|
41
|
+
- No dependency array needed
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Basic usage
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { useRefEffect } from "aidos-ui";
|
|
49
|
+
|
|
50
|
+
function ResizeObserverComponent() {
|
|
51
|
+
const ref = useRefEffect((element) => {
|
|
52
|
+
const observer = new ResizeObserver((entries) => {
|
|
53
|
+
console.log("Size changed:", entries[0].contentRect);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
observer.observe(element);
|
|
57
|
+
|
|
58
|
+
// Return cleanup function
|
|
59
|
+
return () => observer.disconnect();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return <div ref={ref}>Resize me!</div>;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Event listeners
|
|
69
|
+
|
|
70
|
+
Perfect for attaching event listeners to elements:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
function DraggableElement() {
|
|
74
|
+
const ref = useRefEffect((element) => {
|
|
75
|
+
let isDragging = false;
|
|
76
|
+
let startX = 0;
|
|
77
|
+
let startY = 0;
|
|
78
|
+
|
|
79
|
+
const onMouseDown = (e) => {
|
|
80
|
+
isDragging = true;
|
|
81
|
+
startX = e.clientX;
|
|
82
|
+
startY = e.clientY;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const onMouseMove = (e) => {
|
|
86
|
+
if (!isDragging) return;
|
|
87
|
+
const dx = e.clientX - startX;
|
|
88
|
+
const dy = e.clientY - startY;
|
|
89
|
+
element.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onMouseUp = () => {
|
|
93
|
+
isDragging = false;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
element.addEventListener("mousedown", onMouseDown);
|
|
97
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
98
|
+
window.addEventListener("mouseup", onMouseUp);
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
element.removeEventListener("mousedown", onMouseDown);
|
|
102
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
103
|
+
window.removeEventListener("mouseup", onMouseUp);
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return <div ref={ref}>Drag me</div>;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## DOM measurements
|
|
114
|
+
|
|
115
|
+
Use for operations that need the actual DOM element:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
function MeasuredElement() {
|
|
119
|
+
const [dimensions, setDimensions] = useState(null);
|
|
120
|
+
|
|
121
|
+
const ref = useRefEffect((element) => {
|
|
122
|
+
const rect = element.getBoundingClientRect();
|
|
123
|
+
setDimensions({
|
|
124
|
+
width: rect.width,
|
|
125
|
+
height: rect.height,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div ref={ref}>
|
|
131
|
+
{dimensions && (
|
|
132
|
+
<span>
|
|
133
|
+
{dimensions.width}x{dimensions.height}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Third-party library integration
|
|
144
|
+
|
|
145
|
+
Initialize libraries that need a DOM element:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
function ChartComponent({ data }) {
|
|
149
|
+
const ref = useRefEffect((element) => {
|
|
150
|
+
const chart = new SomeChartLibrary(element, {
|
|
151
|
+
data,
|
|
152
|
+
width: 400,
|
|
153
|
+
height: 300,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return () => chart.destroy();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return <div ref={ref} />;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Without cleanup
|
|
166
|
+
|
|
167
|
+
If no cleanup is needed, simply don't return anything:
|
|
4
168
|
|
|
5
169
|
```js
|
|
6
|
-
|
|
170
|
+
const ref = useRefEffect((element) => {
|
|
171
|
+
element.scrollIntoView({ behavior: "smooth" });
|
|
172
|
+
// No cleanup needed
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
7
177
|
|
|
8
|
-
|
|
9
|
-
key: string;
|
|
10
|
-
metaKey?: boolean;
|
|
11
|
-
ctrlKey?: boolean;
|
|
12
|
-
action: (root: T) => void;
|
|
13
|
-
onlyWhenFocused?: boolean;
|
|
14
|
-
};
|
|
178
|
+
## Building other hooks
|
|
15
179
|
|
|
180
|
+
`useRefEffect` is the foundation for other hooks in this library:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
// useKeyboard is built on useRefEffect
|
|
16
184
|
export function useKeyboard<T extends HTMLElement>(
|
|
17
185
|
shortcuts: Array<KeyboardShortcut<T>>
|
|
18
186
|
) {
|
|
19
187
|
return useRefEffect<T>((root: T) => {
|
|
20
188
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
21
|
-
|
|
22
|
-
metaKey,
|
|
23
|
-
key,
|
|
24
|
-
action,
|
|
25
|
-
ctrlKey,
|
|
26
|
-
onlyWhenFocused,
|
|
27
|
-
} of shortcuts) {
|
|
28
|
-
if (metaKey && e.metaKey !== true) {
|
|
29
|
-
continue;
|
|
30
|
-
} else if (ctrlKey && e.ctrlKey !== true) {
|
|
31
|
-
continue;
|
|
32
|
-
} else if (onlyWhenFocused && document.activeElement !== root) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (e.key.toLowerCase() === key.toLowerCase()) {
|
|
37
|
-
action(root);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
189
|
+
// Handle shortcuts...
|
|
40
190
|
};
|
|
41
191
|
|
|
42
192
|
window.addEventListener("keydown", onKeyDown);
|
|
@@ -47,3 +197,55 @@ export function useKeyboard<T extends HTMLElement>(
|
|
|
47
197
|
});
|
|
48
198
|
}
|
|
49
199
|
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## TypeScript usage
|
|
204
|
+
|
|
205
|
+
The hook is generic and preserves element types:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// Typed as HTMLInputElement
|
|
209
|
+
const inputRef = useRefEffect<HTMLInputElement>((input) => {
|
|
210
|
+
input.focus();
|
|
211
|
+
input.select();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Typed as HTMLCanvasElement
|
|
215
|
+
const canvasRef = useRefEffect<HTMLCanvasElement>((canvas) => {
|
|
216
|
+
const ctx = canvas.getContext("2d");
|
|
217
|
+
// ctx is properly typed
|
|
218
|
+
});
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Comparison with useEffect
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
// ❌ useEffect - may run multiple times in dev/strict mode
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const element = ref.current;
|
|
229
|
+
if (!element) return;
|
|
230
|
+
|
|
231
|
+
element.addEventListener("click", handler);
|
|
232
|
+
return () => element.removeEventListener("click", handler);
|
|
233
|
+
}, []);
|
|
234
|
+
|
|
235
|
+
// ✅ useRefEffect - runs exactly once
|
|
236
|
+
const ref = useRefEffect((element) => {
|
|
237
|
+
element.addEventListener("click", handler);
|
|
238
|
+
return () => element.removeEventListener("click", handler);
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## How it works
|
|
245
|
+
|
|
246
|
+
The hook uses a ref callback combined with a cleanup ref:
|
|
247
|
+
|
|
248
|
+
1. When the element mounts, the callback receives the element
|
|
249
|
+
2. The callback's return value (cleanup function) is stored
|
|
250
|
+
3. When the element unmounts (ref receives `null`), cleanup runs
|
|
251
|
+
4. The callback is memoized with `useCallback` to prevent re-runs
|
package/tsconfig.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"lib": [
|
|
3
|
+
"lib": [
|
|
4
|
+
"dom",
|
|
5
|
+
"dom.iterable",
|
|
6
|
+
"es2020",
|
|
7
|
+
"es2020.string"
|
|
8
|
+
],
|
|
4
9
|
"target": "ES2020",
|
|
5
10
|
"module": "esnext",
|
|
6
11
|
"sourceMap": true,
|
|
7
12
|
"outDir": "dist/",
|
|
8
|
-
"jsx": "
|
|
13
|
+
"jsx": "react-jsx",
|
|
9
14
|
"moduleResolution": "node",
|
|
10
15
|
"esModuleInterop": true,
|
|
11
16
|
"declaration": true,
|
|
@@ -18,6 +23,10 @@
|
|
|
18
23
|
"resolveJsonModule": true,
|
|
19
24
|
"isolatedModules": true
|
|
20
25
|
},
|
|
21
|
-
"include": [
|
|
22
|
-
|
|
26
|
+
"include": [
|
|
27
|
+
"src/*"
|
|
28
|
+
],
|
|
29
|
+
"exclude": [
|
|
30
|
+
"node_modules"
|
|
31
|
+
]
|
|
23
32
|
}
|