@fringeworks/react-defer-rendering 0.8.0 → 0.9.0

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.
Files changed (172) hide show
  1. package/README.ja.md +313 -319
  2. package/README.md +313 -319
  3. package/_usePromiseState.cjs +13 -20
  4. package/_usePromiseState.d.cts +5 -2
  5. package/_usePromiseState.d.mts +6 -3
  6. package/_usePromiseState.mjs +10 -16
  7. package/_virtual/_rolldown/runtime.cjs +1 -2
  8. package/constants.cjs +0 -1
  9. package/constants.d.cts +3 -4
  10. package/constants.d.mts +3 -4
  11. package/index.cjs +41 -44
  12. package/index.d.cts +15 -4
  13. package/index.d.mts +15 -4
  14. package/index.mjs +4 -7
  15. package/package.json +113 -31
  16. package/types.cjs +1 -0
  17. package/types.d.cts +7 -8
  18. package/types.d.mts +7 -8
  19. package/useDeferUntilAsyncComplete/index.cjs +4 -4
  20. package/useDeferUntilAsyncComplete/index.d.cts +1 -1
  21. package/useDeferUntilAsyncComplete/index.d.mts +1 -1
  22. package/useDeferUntilAsyncComplete/index.mjs +1 -3
  23. package/useDeferUntilAsyncComplete/types.cjs +1 -0
  24. package/useDeferUntilAsyncComplete/types.d.cts +3 -4
  25. package/useDeferUntilAsyncComplete/types.d.mts +3 -4
  26. package/useDeferUntilAsyncComplete/useDeferUntilAsyncComplete.cjs +7 -5
  27. package/useDeferUntilAsyncComplete/useDeferUntilAsyncComplete.d.cts +0 -1
  28. package/useDeferUntilAsyncComplete/useDeferUntilAsyncComplete.d.mts +2 -4
  29. package/useDeferUntilAsyncComplete/useDeferUntilAsyncComplete.mjs +2 -1
  30. package/useDeferUntilBreakpoint/index.cjs +4 -4
  31. package/useDeferUntilBreakpoint/index.d.cts +1 -1
  32. package/useDeferUntilBreakpoint/index.d.mts +1 -1
  33. package/useDeferUntilBreakpoint/index.mjs +1 -3
  34. package/useDeferUntilBreakpoint/types.cjs +1 -0
  35. package/useDeferUntilBreakpoint/types.d.cts +5 -5
  36. package/useDeferUntilBreakpoint/types.d.mts +5 -5
  37. package/useDeferUntilBreakpoint/useDeferUntilBreakpoint.cjs +29 -9
  38. package/useDeferUntilBreakpoint/useDeferUntilBreakpoint.d.cts +0 -1
  39. package/useDeferUntilBreakpoint/useDeferUntilBreakpoint.d.mts +2 -4
  40. package/useDeferUntilBreakpoint/useDeferUntilBreakpoint.mjs +25 -6
  41. package/useDeferUntilCallThreshold/index.cjs +4 -4
  42. package/useDeferUntilCallThreshold/index.d.cts +1 -1
  43. package/useDeferUntilCallThreshold/index.d.mts +1 -1
  44. package/useDeferUntilCallThreshold/index.mjs +1 -3
  45. package/useDeferUntilCallThreshold/types.cjs +1 -0
  46. package/useDeferUntilCallThreshold/types.d.cts +3 -4
  47. package/useDeferUntilCallThreshold/types.d.mts +3 -4
  48. package/useDeferUntilCallThreshold/useDeferUntilCallThreshold.cjs +32 -19
  49. package/useDeferUntilCallThreshold/useDeferUntilCallThreshold.d.cts +0 -1
  50. package/useDeferUntilCallThreshold/useDeferUntilCallThreshold.d.mts +2 -4
  51. package/useDeferUntilCallThreshold/useDeferUntilCallThreshold.mjs +27 -15
  52. package/useDeferUntilChange/index.cjs +4 -4
  53. package/useDeferUntilChange/index.d.cts +1 -1
  54. package/useDeferUntilChange/index.d.mts +1 -1
  55. package/useDeferUntilChange/index.mjs +1 -3
  56. package/useDeferUntilChange/types.cjs +1 -0
  57. package/useDeferUntilChange/types.d.cts +3 -4
  58. package/useDeferUntilChange/types.d.mts +3 -4
  59. package/useDeferUntilChange/useDeferUntilChange.cjs +6 -9
  60. package/useDeferUntilChange/useDeferUntilChange.d.cts +0 -1
  61. package/useDeferUntilChange/useDeferUntilChange.d.mts +2 -4
  62. package/useDeferUntilChange/useDeferUntilChange.mjs +2 -4
  63. package/useDeferUntilDate/index.cjs +4 -4
  64. package/useDeferUntilDate/index.d.cts +1 -1
  65. package/useDeferUntilDate/index.d.mts +1 -1
  66. package/useDeferUntilDate/index.mjs +1 -3
  67. package/useDeferUntilDate/types.cjs +1 -0
  68. package/useDeferUntilDate/types.d.cts +3 -4
  69. package/useDeferUntilDate/types.d.mts +3 -4
  70. package/useDeferUntilDate/useDeferUntilDate.cjs +13 -4
  71. package/useDeferUntilDate/useDeferUntilDate.d.cts +0 -1
  72. package/useDeferUntilDate/useDeferUntilDate.d.mts +2 -4
  73. package/useDeferUntilDate/useDeferUntilDate.mjs +10 -4
  74. package/useDeferUntilIntersected/index.cjs +4 -4
  75. package/useDeferUntilIntersected/index.d.cts +1 -1
  76. package/useDeferUntilIntersected/index.d.mts +1 -1
  77. package/useDeferUntilIntersected/index.mjs +1 -3
  78. package/useDeferUntilIntersected/types.cjs +1 -0
  79. package/useDeferUntilIntersected/types.d.cts +8 -9
  80. package/useDeferUntilIntersected/types.d.mts +8 -9
  81. package/useDeferUntilIntersected/useDeferUntilIntersected.cjs +14 -13
  82. package/useDeferUntilIntersected/useDeferUntilIntersected.d.cts +0 -1
  83. package/useDeferUntilIntersected/useDeferUntilIntersected.d.mts +2 -4
  84. package/useDeferUntilIntersected/useDeferUntilIntersected.mjs +10 -8
  85. package/useDeferUntilOnReady/index.cjs +4 -4
  86. package/useDeferUntilOnReady/index.d.cts +1 -1
  87. package/useDeferUntilOnReady/index.d.mts +1 -1
  88. package/useDeferUntilOnReady/index.mjs +1 -3
  89. package/useDeferUntilOnReady/types.cjs +1 -0
  90. package/useDeferUntilOnReady/types.d.cts +3 -4
  91. package/useDeferUntilOnReady/types.d.mts +3 -4
  92. package/useDeferUntilOnReady/useDeferUntilOnReady.cjs +28 -21
  93. package/useDeferUntilOnReady/useDeferUntilOnReady.d.cts +2 -2
  94. package/useDeferUntilOnReady/useDeferUntilOnReady.d.mts +4 -5
  95. package/useDeferUntilOnReady/useDeferUntilOnReady.mjs +23 -15
  96. package/useDeferUntilReady/index.cjs +4 -4
  97. package/useDeferUntilReady/index.d.cts +1 -1
  98. package/useDeferUntilReady/index.d.mts +1 -1
  99. package/useDeferUntilReady/index.mjs +1 -3
  100. package/useDeferUntilReady/types.cjs +1 -0
  101. package/useDeferUntilReady/types.d.cts +2 -4
  102. package/useDeferUntilReady/types.d.mts +2 -4
  103. package/useDeferUntilReady/useDeferUntilReady.cjs +20 -32
  104. package/useDeferUntilReady/useDeferUntilReady.d.cts +0 -1
  105. package/useDeferUntilReady/useDeferUntilReady.d.mts +2 -4
  106. package/useDeferUntilReady/useDeferUntilReady.mjs +18 -29
  107. package/useDeferUntilRender/index.cjs +4 -4
  108. package/useDeferUntilRender/index.d.cts +1 -1
  109. package/useDeferUntilRender/index.d.mts +1 -1
  110. package/useDeferUntilRender/index.mjs +1 -3
  111. package/useDeferUntilRender/types.cjs +1 -0
  112. package/useDeferUntilRender/types.d.cts +5 -5
  113. package/useDeferUntilRender/types.d.mts +5 -5
  114. package/useDeferUntilRender/useDeferUntilRender.cjs +31 -12
  115. package/useDeferUntilRender/useDeferUntilRender.d.cts +0 -1
  116. package/useDeferUntilRender/useDeferUntilRender.d.mts +2 -4
  117. package/useDeferUntilRender/useDeferUntilRender.mjs +26 -8
  118. package/useDeferUntilResolved/index.cjs +4 -4
  119. package/useDeferUntilResolved/index.d.cts +1 -1
  120. package/useDeferUntilResolved/index.d.mts +1 -1
  121. package/useDeferUntilResolved/index.mjs +1 -3
  122. package/useDeferUntilResolved/types.cjs +1 -0
  123. package/useDeferUntilResolved/types.d.cts +3 -4
  124. package/useDeferUntilResolved/types.d.mts +3 -4
  125. package/useDeferUntilResolved/useDeferUntilResolved.cjs +8 -5
  126. package/useDeferUntilResolved/useDeferUntilResolved.d.cts +0 -1
  127. package/useDeferUntilResolved/useDeferUntilResolved.d.mts +2 -4
  128. package/useDeferUntilResolved/useDeferUntilResolved.mjs +3 -1
  129. package/useDeferUntilTimeout/index.cjs +4 -4
  130. package/useDeferUntilTimeout/index.d.cts +1 -1
  131. package/useDeferUntilTimeout/index.d.mts +1 -1
  132. package/useDeferUntilTimeout/index.mjs +1 -3
  133. package/useDeferUntilTimeout/types.cjs +1 -0
  134. package/useDeferUntilTimeout/types.d.cts +3 -4
  135. package/useDeferUntilTimeout/types.d.mts +3 -4
  136. package/useDeferUntilTimeout/useDeferUntilTimeout.cjs +23 -21
  137. package/useDeferUntilTimeout/useDeferUntilTimeout.d.cts +0 -1
  138. package/useDeferUntilTimeout/useDeferUntilTimeout.d.mts +2 -4
  139. package/useDeferUntilTimeout/useDeferUntilTimeout.mjs +16 -14
  140. package/useDeferUntilTrue/index.cjs +4 -4
  141. package/useDeferUntilTrue/index.d.cts +1 -1
  142. package/useDeferUntilTrue/index.d.mts +1 -1
  143. package/useDeferUntilTrue/index.mjs +1 -3
  144. package/useDeferUntilTrue/types.cjs +1 -0
  145. package/useDeferUntilTrue/types.d.cts +2 -4
  146. package/useDeferUntilTrue/types.d.mts +2 -4
  147. package/useDeferUntilTrue/useDeferUntilTrue.cjs +6 -5
  148. package/useDeferUntilTrue/useDeferUntilTrue.d.cts +0 -1
  149. package/useDeferUntilTrue/useDeferUntilTrue.d.mts +2 -4
  150. package/useDeferUntilWebFontReady/index.cjs +4 -4
  151. package/useDeferUntilWebFontReady/index.d.cts +1 -1
  152. package/useDeferUntilWebFontReady/index.d.mts +1 -1
  153. package/useDeferUntilWebFontReady/index.mjs +1 -3
  154. package/useDeferUntilWebFontReady/types.cjs +1 -0
  155. package/useDeferUntilWebFontReady/types.d.cts +6 -7
  156. package/useDeferUntilWebFontReady/types.d.mts +6 -7
  157. package/useDeferUntilWebFontReady/useDeferUntilWebFontReady.cjs +23 -14
  158. package/useDeferUntilWebFontReady/useDeferUntilWebFontReady.d.cts +0 -1
  159. package/useDeferUntilWebFontReady/useDeferUntilWebFontReady.d.mts +2 -4
  160. package/useDeferUntilWebFontReady/useDeferUntilWebFontReady.mjs +16 -9
  161. package/useDeferUntilScrolled/index.cjs +0 -5
  162. package/useDeferUntilScrolled/index.d.cts +0 -3
  163. package/useDeferUntilScrolled/index.d.mts +0 -3
  164. package/useDeferUntilScrolled/index.mjs +0 -5
  165. package/useDeferUntilScrolled/types.cjs +0 -0
  166. package/useDeferUntilScrolled/types.d.cts +0 -29
  167. package/useDeferUntilScrolled/types.d.mts +0 -29
  168. package/useDeferUntilScrolled/types.mjs +0 -1
  169. package/useDeferUntilScrolled/useDeferUntilScrolled.cjs +0 -94
  170. package/useDeferUntilScrolled/useDeferUntilScrolled.d.cts +0 -14
  171. package/useDeferUntilScrolled/useDeferUntilScrolled.d.mts +0 -15
  172. package/useDeferUntilScrolled/useDeferUntilScrolled.mjs +0 -91
package/README.md CHANGED
@@ -1,319 +1,313 @@
1
- # @fringeworks/react-defer-rendering
2
-
3
- `@fringeworks/react-defer-rendering` is a niche library specialized in deferring the rendering of components until a specified condition is met.\
4
- It provides a set of hooks covering a wide range of conditions, including timers, Promises, and browser APIs (`matchMedia`, `IntersectionObserver`, etc.).
5
-
6
- **[日本語のREADMEはこちら](./README.ja.md)**
7
-
8
- ## Installation
9
-
10
- ```bash
11
- npm install @fringeworks/react-defer-rendering
12
- # or
13
- pnpm add @fringeworks/react-defer-rendering
14
- ```
15
-
16
- ## Usage
17
-
18
- Each hook returns a `state` (`'pending' | 'fallback' | 'ready'`) representing the rendering status of the target node, along with the `node` that should be rendered for that state.
19
-
20
- ```tsx
21
- import { useDeferUntilTrue } from '@fringeworks/react-defer-rendering';
22
-
23
- const { node } = useDeferUntilTrue(<MyComponent />, isReady, {
24
- pending: <Spinner />,
25
- });
26
-
27
- return node;
28
- ```
29
-
30
- - While the condition is not met (`pending`), the node specified in the `pending` option is rendered
31
- - Once the condition is met (`ready`), rendering switches to the target node passed as the first argument
32
- - Some hooks also support a failure state (`fallback`)
33
-
34
- ### When the target to wait for is not specified
35
-
36
- Hooks that take something to wait for become `ready` immediately when the second argument is `null` / `undefined`, since there is nothing to wait for. If it later becomes unspecified, the state returns to `ready`; if it changes from unspecified to a value, the hook starts waiting again from `pending`.\
37
- Because hooks cannot be called conditionally, you can make deferred rendering an optional feature of your component by passing `undefined` as the second argument.
38
-
39
- | Hook | Second argument treated as unspecified |
40
- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
41
- | `useDeferUntilTimeout`, `useDeferUntilDate`, `useDeferUntilResolved`, `useDeferUntilAsyncComplete` | `null` / `undefined` |
42
- | `useDeferUntilBreakpoint`, `useDeferUntilRender` | `null` / `undefined` / empty string |
43
- | `useDeferUntilWebFontReady` | `null` / `undefined` / empty string / empty array |
44
- | `useDeferUntilIntersected`, `useDeferUntilScrolled` | The ref object itself is `null` / `undefined` |
45
-
46
- For `useDeferUntilIntersected` and `useDeferUntilScrolled`, a ref whose `current` is `null` means the element has not been mounted yet, so the state stays `pending`.
47
-
48
- ```tsx
49
- function Heading({ fontFamily, children }: Props) {
50
- // Render immediately when fontFamily is not specified
51
- const { node } = useDeferUntilWebFontReady(<h1>{children}</h1>, fontFamily, {
52
- pending: null,
53
- });
54
- return node;
55
- }
56
- ```
57
-
58
- Note that the second argument of `useDeferUntilTrue` is the condition itself, so `null` / `undefined` is treated as unmet (`pending`).
59
-
60
- ## Basic Hooks
61
-
62
- ### `useDeferUntilReady`
63
-
64
- The most fundamental hook, controlling rendering by directly specifying a `state` (`'pending' | 'fallback' | 'ready'`). All other hooks are built on top of this one.
65
-
66
- ```tsx
67
- import { useDeferUntilReady } from '@fringeworks/react-defer-rendering';
68
-
69
- const { node } = useDeferUntilReady(<MyComponent />, state, {
70
- pending: <Spinner />,
71
- fallback: <ErrorMessage />,
72
- });
73
- ```
74
-
75
- ### `useDeferUntilTrue`
76
-
77
- Defers rendering until a boolean condition becomes `true`.
78
-
79
- ```tsx
80
- import { useDeferUntilTrue } from '@fringeworks/react-defer-rendering';
81
-
82
- const { node } = useDeferUntilTrue(<MyComponent />, isReady, {
83
- pending: <Spinner />,
84
- });
85
- ```
86
-
87
- ## Waiting for Elapsed Time
88
-
89
- ### `useDeferUntilTimeout`
90
-
91
- Defers rendering until the specified duration (in milliseconds) has elapsed.
92
-
93
- ```tsx
94
- import { useDeferUntilTimeout } from '@fringeworks/react-defer-rendering';
95
-
96
- const { node } = useDeferUntilTimeout(<MyComponent />, 3000, {
97
- pending: <Spinner />,
98
- });
99
- ```
100
-
101
- ### `useDeferUntilDate`
102
-
103
- Defers rendering until the specified date and time.
104
-
105
- ```tsx
106
- import { useDeferUntilDate } from '@fringeworks/react-defer-rendering';
107
-
108
- const { node } = useDeferUntilDate(
109
- <Campaign />,
110
- new Date('2026-01-01T00:00:00'),
111
- { pending: <ComingSoon /> },
112
- );
113
- ```
114
-
115
- ## Waiting for Async Completion
116
-
117
- ### `useDeferUntilResolved`
118
-
119
- Defers rendering until a Promise settles (resolves or rejects).
120
-
121
- ```tsx
122
- import { useDeferUntilResolved } from '@fringeworks/react-defer-rendering';
123
-
124
- const { node } = useDeferUntilResolved(<MyComponent />, fetchPromise, {
125
- pending: <Spinner />,
126
- fallback: <ErrorMessage />,
127
- });
128
- ```
129
-
130
- ### `useDeferUntilAsyncComplete`
131
-
132
- Defers rendering until an async function finishes executing. `asyncFn` is invoked after mount (inside an effect), which also manages the creation of the Promise itself. It is not invoked during SSR.\nSince `asyncFn` is re-invoked whenever its reference changes, memoize it with `useCallback` or similar.
133
-
134
- ```tsx
135
- import { useDeferUntilAsyncComplete } from '@fringeworks/react-defer-rendering';
136
- import { useCallback } from 'react';
137
-
138
- const loadData = useCallback(
139
- () => fetch('/api/data').then((res) => res.json()),
140
- [],
141
- );
142
- const { node } = useDeferUntilAsyncComplete(<MyComponent />, loadData, {
143
- pending: <Spinner />,
144
- });
145
- ```
146
-
147
- ## Waiting for a Value to Change
148
-
149
- ### `useDeferUntilChange`
150
-
151
- Defers rendering until the specified value changes. Each time the value changes, it briefly passes through the `pending` state.
152
-
153
- ```tsx
154
- import { useDeferUntilChange } from '@fringeworks/react-defer-rendering';
155
-
156
- const { node } = useDeferUntilChange(<Toast>{message}</Toast>, message, {
157
- pending: null,
158
- });
159
- ```
160
-
161
- ## Manual Control at Any Timing
162
-
163
- ### `useDeferUntilOnReady`
164
-
165
- Lets you control the state at any timing by calling the returned `onReady` / `onFallback` / `onPending` handlers. Useful for conditions the hook itself cannot detect, such as event handlers.
166
-
167
- ```tsx
168
- import { useDeferUntilOnReady } from '@fringeworks/react-defer-rendering';
169
-
170
- const { node, onReady } = useDeferUntilOnReady(<Video />, {
171
- pending: <Spinner />,
172
- });
173
-
174
- <video onCanPlay={onReady}>{node}</video>;
175
- ```
176
-
177
- ### `useDeferUntilCallThreshold`
178
-
179
- Similar to `useDeferUntilOnReady`, but the state only switches once each handler has been called the specified number of times.
180
-
181
- ```tsx
182
- import { useDeferUntilCallThreshold } from '@fringeworks/react-defer-rendering';
183
-
184
- const { node, onReady } = useDeferUntilCallThreshold(<Gallery />, {
185
- pending: <Spinner />,
186
- onReadyCount: 3, // becomes ready once onReady has been called 3 times
187
- });
188
- ```
189
-
190
- ## Waiting for Browser State
191
-
192
- > **Note (SSR / RSC):** The hooks listed here depend on browser APIs, so their actual state cannot be determined on the server. See [Using with SSR / RSC](#using-with-ssr--rsc) for details.
193
-
194
- ### `useDeferUntilBreakpoint`
195
-
196
- Defers rendering until a media query matches.
197
-
198
- ```tsx
199
- import { useDeferUntilBreakpoint } from '@fringeworks/react-defer-rendering';
200
-
201
- const { node } = useDeferUntilBreakpoint(
202
- <DesktopNav />,
203
- '(min-width: 1024px)',
204
- { pending: <MobileNav /> },
205
- );
206
- ```
207
-
208
- ### `useDeferUntilIntersected`
209
-
210
- Defers rendering until a reference element enters the viewport.
211
-
212
- ```tsx
213
- import { useRef } from 'react';
214
- import { useDeferUntilIntersected } from '@fringeworks/react-defer-rendering';
215
-
216
- const elementRef = useRef<HTMLDivElement>(null);
217
- const { node } = useDeferUntilIntersected(<HeavyChart />, elementRef, {
218
- pending: <Placeholder />,
219
- });
220
-
221
- <div ref={elementRef}>{node}</div>;
222
- ```
223
-
224
- ### `useDeferUntilScrolled`
225
-
226
- Defers rendering until a reference element becomes visible as a result of scrolling.
227
-
228
- ```tsx
229
- const { node } = useDeferUntilScrolled(<LazyImage />, elementRef, {
230
- pending: <Placeholder />,
231
- rootMargin: 100,
232
- });
233
- ```
234
-
235
- ### `useDeferUntilRender`
236
-
237
- Defers rendering until an element matching the selector is rendered into the DOM. Useful for waiting on elements outside of your own control, such as ones inserted by third-party scripts.
238
-
239
- ```tsx
240
- import { useDeferUntilRender } from '@fringeworks/react-defer-rendering';
241
-
242
- const { node } = useDeferUntilRender(<Overlay />, '#third-party-widget', {
243
- pending: null,
244
- });
245
- ```
246
-
247
- ### `useDeferUntilWebFontReady`
248
-
249
- Defers rendering until all specified web fonts become available. Web fonts are checked with [`@fringeworks/web-font-observer`](https://www.npmjs.com/package/@fringeworks/web-font-observer).\
250
- It targets web fonts defined with `@font-face`. System fonts cannot be detected as loaded, so specifying one results in `fallback` after the timeout.
251
-
252
- ```tsx
253
- import { useDeferUntilWebFontReady } from '@fringeworks/react-defer-rendering';
254
-
255
- const { node } = useDeferUntilWebFontReady(
256
- <Heading>Title</Heading>,
257
- 'Noto Sans JP',
258
- {
259
- pending: <Heading style={{ visibility: 'hidden' }}>Title</Heading>,
260
- },
261
- );
262
- ```
263
-
264
- To wait for multiple web fonts, pass an array. To specify the weight and so on, pass an object. `null` / `undefined` in the array are ignored, so you can add web fonts conditionally.\
265
- Arrays and objects can be written inline (waiting does not restart unless their contents change).
266
-
267
- ```tsx
268
- const { node } = useDeferUntilWebFontReady(<Article />, [
269
- { family: 'Noto Sans JP', text: 'あ' },
270
- { family: 'Roboto', weight: 400 },
271
- isBold ? { family: 'Roboto', weight: 700 } : null,
272
- ]);
273
- ```
274
-
275
- | Option | Type | Default | Description |
276
- | --------------- | ---------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- |
277
- | `timeout?` | `number` | `3000` | Time in milliseconds before the state becomes `fallback` when a web font is not available |
278
- | `loader?` | `(signal: AbortSignal) => Promise<void>` | - | Function that loads the web fonts. Checking starts after it completes. Called only when the fonts change |
279
- | `initialState?` | `'pending' \| 'ready' \| 'fallback'` | `'pending'` | Initial state in environments where the loading state cannot be determined, such as SSR |
280
-
281
- `weight` / `style` / `width` / `text` in the object form are the same as in `@fringeworks/web-font-observer`.
282
-
283
- - If any web font does not become available, the state becomes `fallback` and the remaining waits are aborted.
284
- - Web fonts that are already loaded become `ready` without waiting (no flicker for cached fonts).
285
- - If a web font finishes loading after the state became `fallback`, the state becomes `ready`. Use `preserveOnceFallback` to keep `fallback`.
286
- - Place the `@font-face` definitions outside the deferred node (e.g. global CSS or a layout). If they are inside the deferred node, they are never registered and the hook waits until the timeout.
287
-
288
- ## Common Options
289
-
290
- ### Options for `pending` / `ready`
291
-
292
- These options are shared across all hooks.
293
-
294
- | Option | Type | Description |
295
- | -------------------- | ----------- | ---------------------------------------------- |
296
- | `pending?` | `ReactNode` | Node rendered while waiting for the condition |
297
- | `pendingDefer?` | `number` | Delay (in ms) before showing `pending` |
298
- | `readyDefer?` | `number` | Delay (in ms) before showing the target node |
299
- | `preserveOnceReady?` | `boolean` | Whether to keep the `ready` state once reached |
300
-
301
- ### Options for `fallback`
302
-
303
- `useDeferUntilReady`, `useDeferUntilResolved`, `useDeferUntilAsyncComplete`, `useDeferUntilOnReady`, `useDeferUntilCallThreshold`, and `useDeferUntilWebFontReady` also support a `fallback` state for failures.
304
-
305
- | Option | Type | Description |
306
- | ----------------------- | ----------- | ------------------------------------------------- |
307
- | `fallback?` | `ReactNode` | Node rendered on failure |
308
- | `fallbackDefer?` | `number` | Delay (in ms) before showing `fallback` |
309
- | `preserveOnceFallback?` | `boolean` | Whether to keep the `fallback` state once reached |
310
-
311
- ## Using with SSR / RSC
312
-
313
- Every hook in this library ships with a `'use client'` directive attached at build time. In React Server Components environments such as Next.js App Router, they are treated as client component boundaries without any additional configuration.
314
-
315
- `useDeferUntilBreakpoint`, `useDeferUntilIntersected`, `useDeferUntilScrolled`, and `useDeferUntilRender` depend on browser APIs (`matchMedia`, `IntersectionObserver`, `MutationObserver`, etc.), so their actual state cannot be determined on the server. These hooks accept an `initialCondition` option to specify the value used during SSR (it is treated as `false` if not specified). Similarly, `useDeferUntilWebFontReady` accepts an `initialState` option to specify its initial state during SSR.
316
-
317
- ## License
318
-
319
- MIT
1
+ # @fringeworks/react-defer-rendering
2
+
3
+ `@fringeworks/react-defer-rendering` is a library some will find handy, specialized in deferring the rendering of components until a specified condition is met.\
4
+ It provides a set of hooks covering a wide range of conditions, including timers, Promises, and browser APIs (`matchMedia`, `IntersectionObserver`, etc.).
5
+
6
+ **[日本語のREADMEはこちら](./README.ja.md)**
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @fringeworks/react-defer-rendering
12
+ # or
13
+ pnpm add @fringeworks/react-defer-rendering
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ Each hook returns a `state` (`'pending' | 'fallback' | 'ready'`) representing the rendering status of the target node, along with the `node` that should be rendered for that state.
19
+
20
+ ```tsx
21
+ import { useDeferUntilTrue } from '@fringeworks/react-defer-rendering';
22
+
23
+ const { node } = useDeferUntilTrue(<MyComponent />, isReady, {
24
+ pending: <Spinner />,
25
+ });
26
+
27
+ return node;
28
+ ```
29
+
30
+ - While the condition is not met (`pending`), the node specified in the `pending` option is rendered
31
+ - Once the condition is met (`ready`), rendering switches to the target node passed as the first argument
32
+ - Some hooks also support a failure state (`fallback`)
33
+
34
+ ### When the target to wait for is not specified
35
+
36
+ Hooks that take something to wait for become `ready` immediately when the second argument is `null` / `undefined`, since there is nothing to wait for. If it later becomes unspecified, the state returns to `ready`; if it changes from unspecified to a value, the hook starts waiting again from `pending`.\
37
+ Because hooks cannot be called conditionally, you can make deferred rendering an optional feature of your component by passing `undefined` as the second argument.
38
+
39
+ | Hook | Second argument treated as unspecified |
40
+ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
41
+ | `useDeferUntilTimeout`, `useDeferUntilDate`, `useDeferUntilResolved`, `useDeferUntilAsyncComplete` | `null` / `undefined` |
42
+ | `useDeferUntilBreakpoint`, `useDeferUntilRender` | `null` / `undefined` / empty string |
43
+ | `useDeferUntilWebFontReady` | `null` / `undefined` / empty string / empty array |
44
+ | `useDeferUntilIntersected` | The ref object itself is `null` / `undefined` |
45
+
46
+ For `useDeferUntilIntersected`, a ref whose `current` is `null` means the element has not been mounted yet, so the state stays `pending`.
47
+
48
+ ```tsx
49
+ function Heading({ fontFamily, children }: Props) {
50
+ // Render immediately when fontFamily is not specified
51
+ const { node } = useDeferUntilWebFontReady(<h1>{children}</h1>, fontFamily, {
52
+ pending: null,
53
+ });
54
+ return node;
55
+ }
56
+ ```
57
+
58
+ Note that the second argument of `useDeferUntilTrue` is the condition itself, so `null` / `undefined` is treated as unmet (`pending`).
59
+
60
+ ## Basic Hooks
61
+
62
+ ### `useDeferUntilReady`
63
+
64
+ The most fundamental hook, controlling rendering by directly specifying a `state` (`'pending' | 'fallback' | 'ready'`). All other hooks are built on top of this one.
65
+
66
+ ```tsx
67
+ import { useDeferUntilReady } from '@fringeworks/react-defer-rendering';
68
+
69
+ const { node } = useDeferUntilReady(<MyComponent />, state, {
70
+ pending: <Spinner />,
71
+ fallback: <ErrorMessage />,
72
+ });
73
+ ```
74
+
75
+ ### `useDeferUntilTrue`
76
+
77
+ Defers rendering until a boolean condition becomes `true`.
78
+
79
+ ```tsx
80
+ import { useDeferUntilTrue } from '@fringeworks/react-defer-rendering';
81
+
82
+ const { node } = useDeferUntilTrue(<MyComponent />, isReady, {
83
+ pending: <Spinner />,
84
+ });
85
+ ```
86
+
87
+ ## Waiting for Elapsed Time
88
+
89
+ ### `useDeferUntilTimeout`
90
+
91
+ Defers rendering until the specified duration (in milliseconds) has elapsed.
92
+
93
+ ```tsx
94
+ import { useDeferUntilTimeout } from '@fringeworks/react-defer-rendering';
95
+
96
+ const { node } = useDeferUntilTimeout(<MyComponent />, 3000, {
97
+ pending: <Spinner />,
98
+ });
99
+ ```
100
+
101
+ ### `useDeferUntilDate`
102
+
103
+ Defers rendering until the specified date and time.
104
+
105
+ ```tsx
106
+ import { useDeferUntilDate } from '@fringeworks/react-defer-rendering';
107
+
108
+ const { node } = useDeferUntilDate(
109
+ <Campaign />,
110
+ new Date('2026-01-01T00:00:00'),
111
+ { pending: <ComingSoon /> },
112
+ );
113
+ ```
114
+
115
+ ## Waiting for Async Completion
116
+
117
+ ### `useDeferUntilResolved`
118
+
119
+ Defers rendering until a Promise settles (resolves or rejects).\
120
+ Since waiting restarts whenever the Promise reference changes, do not create a new Promise on every render (create it outside the component, or memoize it with `useMemo` or similar).
121
+
122
+ ```tsx
123
+ import { useDeferUntilResolved } from '@fringeworks/react-defer-rendering';
124
+
125
+ const { node } = useDeferUntilResolved(<MyComponent />, fetchPromise, {
126
+ pending: <Spinner />,
127
+ fallback: <ErrorMessage />,
128
+ });
129
+ ```
130
+
131
+ ### `useDeferUntilAsyncComplete`
132
+
133
+ Defers rendering until an async function finishes executing. `asyncFn` is invoked after mount (inside an effect), which also manages the creation of the Promise itself. It is not invoked during SSR.\
134
+ Since `asyncFn` is re-invoked whenever its reference changes, memoize it with `useCallback` or similar.
135
+
136
+ ```tsx
137
+ import { useDeferUntilAsyncComplete } from '@fringeworks/react-defer-rendering';
138
+ import { useCallback } from 'react';
139
+
140
+ const loadData = useCallback(
141
+ () => fetch('/api/data').then((res) => res.json()),
142
+ [],
143
+ );
144
+ const { node } = useDeferUntilAsyncComplete(<MyComponent />, loadData, {
145
+ pending: <Spinner />,
146
+ });
147
+ ```
148
+
149
+ ## Waiting for a Value to Change
150
+
151
+ ### `useDeferUntilChange`
152
+
153
+ Defers rendering until the specified value changes. Each time the value changes, it briefly passes through the `pending` state.
154
+
155
+ ```tsx
156
+ import { useDeferUntilChange } from '@fringeworks/react-defer-rendering';
157
+
158
+ const { node } = useDeferUntilChange(<Toast>{message}</Toast>, message, {
159
+ pending: null,
160
+ });
161
+ ```
162
+
163
+ ## Manual Control at Any Timing
164
+
165
+ ### `useDeferUntilOnReady`
166
+
167
+ Lets you control the state at any timing by calling the returned `onReady` / `onFallback` / `onPending` handlers. Useful for conditions the hook itself cannot detect, such as event handlers.\
168
+ Even when state changes are delayed with `onReadyDelay` / `onFallbackDelay` / `onPendingDelay`, calling any handler cancels the pending change, so the state ends up as the one of the last handler called.
169
+
170
+ ```tsx
171
+ import { useDeferUntilOnReady } from '@fringeworks/react-defer-rendering';
172
+
173
+ const { node, onReady } = useDeferUntilOnReady(<Video />, {
174
+ pending: <Spinner />,
175
+ });
176
+
177
+ <video onCanPlay={onReady}>{node}</video>;
178
+ ```
179
+
180
+ ### `useDeferUntilCallThreshold`
181
+
182
+ Similar to `useDeferUntilOnReady`, but the state only switches once each handler has been called the specified number of times.
183
+
184
+ ```tsx
185
+ import { useDeferUntilCallThreshold } from '@fringeworks/react-defer-rendering';
186
+
187
+ const { node, onReady } = useDeferUntilCallThreshold(<Gallery />, {
188
+ pending: <Spinner />,
189
+ onReadyCount: 3, // becomes ready once onReady has been called 3 times
190
+ });
191
+ ```
192
+
193
+ ## Waiting for Browser State
194
+
195
+ > **Note (SSR / RSC):** The hooks listed here depend on browser APIs, so their actual state cannot be determined on the server. See [Using with SSR / RSC](#using-with-ssr--rsc) for details.
196
+
197
+ ### `useDeferUntilBreakpoint`
198
+
199
+ Defers rendering until a media query matches.
200
+
201
+ ```tsx
202
+ import { useDeferUntilBreakpoint } from '@fringeworks/react-defer-rendering';
203
+
204
+ const { node } = useDeferUntilBreakpoint(
205
+ <DesktopNav />,
206
+ '(min-width: 1024px)',
207
+ { pending: <MobileNav /> },
208
+ );
209
+ ```
210
+
211
+ ### `useDeferUntilIntersected`
212
+
213
+ Defers rendering until a reference element enters the viewport.
214
+
215
+ ```tsx
216
+ import { useRef } from 'react';
217
+ import { useDeferUntilIntersected } from '@fringeworks/react-defer-rendering';
218
+
219
+ const elementRef = useRef<HTMLDivElement>(null);
220
+ const { node } = useDeferUntilIntersected(<HeavyChart />, elementRef, {
221
+ pending: <Placeholder />,
222
+ });
223
+
224
+ <div ref={elementRef}>{node}</div>;
225
+ ```
226
+
227
+ Use `rootMargin` to expand the area to check. A number is in pixels (the same value on all sides), and a string uses the `rootMargin` syntax of `IntersectionObserver` (such as `'100px 0px'` or `'10%'`; only `px` and `%` are allowed). Use `threshold` (0 to 1, defaults to `0.1`) to specify how much of the element must be visible.
228
+
229
+ ### `useDeferUntilRender`
230
+
231
+ Defers rendering until an element matching the selector is rendered into the DOM. Useful for waiting on elements outside of your own control, such as ones inserted by third-party scripts.
232
+
233
+ ```tsx
234
+ import { useDeferUntilRender } from '@fringeworks/react-defer-rendering';
235
+
236
+ const { node } = useDeferUntilRender(<Overlay />, '#third-party-widget', {
237
+ pending: null,
238
+ });
239
+ ```
240
+
241
+ ### `useDeferUntilWebFontReady`
242
+
243
+ Defers rendering until all specified web fonts become available. Web fonts are checked with [`@fringeworks/web-font-observer`](https://www.npmjs.com/package/@fringeworks/web-font-observer).\
244
+ It targets web fonts defined with `@font-face`. System fonts cannot be detected as loaded, so specifying one results in `fallback` after the timeout.
245
+
246
+ ```tsx
247
+ import { useDeferUntilWebFontReady } from '@fringeworks/react-defer-rendering';
248
+
249
+ const { node } = useDeferUntilWebFontReady(
250
+ <Heading>Title</Heading>,
251
+ 'Noto Sans JP',
252
+ {
253
+ pending: <Heading style={{ visibility: 'hidden' }}>Title</Heading>,
254
+ },
255
+ );
256
+ ```
257
+
258
+ To wait for multiple web fonts, pass an array. To specify the weight and so on, pass an object. `null` / `undefined` in the array are ignored, so you can add web fonts conditionally.\
259
+ Arrays and objects can be written inline (waiting does not restart unless their contents change).
260
+
261
+ ```tsx
262
+ const { node } = useDeferUntilWebFontReady(<Article />, [
263
+ { family: 'Noto Sans JP', text: 'あ' },
264
+ { family: 'Roboto', weight: 400 },
265
+ isBold ? { family: 'Roboto', weight: 700 } : null,
266
+ ]);
267
+ ```
268
+
269
+ | Option | Type | Default | Description |
270
+ | --------------- | ---------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- |
271
+ | `timeout?` | `number` | `3000` | Time in milliseconds before the state becomes `fallback` when a web font is not available |
272
+ | `loader?` | `(signal: AbortSignal) => Promise<void>` | - | Function that loads the web fonts. Checking starts after it completes. Called only when the fonts change |
273
+ | `initialState?` | `'pending' \| 'ready' \| 'fallback'` | `'pending'` | Initial state in environments where the loading state cannot be determined, such as SSR |
274
+
275
+ `weight` / `style` / `width` / `text` in the object form are the same as in `@fringeworks/web-font-observer`.
276
+
277
+ - If any web font does not become available, the state becomes `fallback` and the remaining waits are aborted.
278
+ - Web fonts that are already loaded become `ready` without waiting (no flicker for cached fonts).
279
+ - If a web font finishes loading after the state became `fallback`, the state becomes `ready`. Use `preserveOnceFallback` to keep `fallback`.
280
+ - Place the `@font-face` definitions outside the deferred node (e.g. global CSS or a layout). If they are inside the deferred node, they are never registered and the hook waits until the timeout.
281
+
282
+ ## Common Options
283
+
284
+ ### Options for `pending` / `ready`
285
+
286
+ These options are shared across all hooks.
287
+
288
+ | Option | Type | Description |
289
+ | -------------------- | ----------- | ---------------------------------------------- |
290
+ | `pending?` | `ReactNode` | Node rendered while waiting for the condition |
291
+ | `pendingDefer?` | `number` | Delay (in ms) before showing `pending` |
292
+ | `readyDefer?` | `number` | Delay (in ms) before showing the target node |
293
+ | `preserveOnceReady?` | `boolean` | Whether to keep the `ready` state once reached |
294
+
295
+ ### Options for `fallback`
296
+
297
+ `useDeferUntilReady`, `useDeferUntilResolved`, `useDeferUntilAsyncComplete`, `useDeferUntilOnReady`, `useDeferUntilCallThreshold`, and `useDeferUntilWebFontReady` also support a `fallback` state for failures.
298
+
299
+ | Option | Type | Description |
300
+ | ----------------------- | ----------- | ------------------------------------------------- |
301
+ | `fallback?` | `ReactNode` | Node rendered on failure |
302
+ | `fallbackDefer?` | `number` | Delay (in ms) before showing `fallback` |
303
+ | `preserveOnceFallback?` | `boolean` | Whether to keep the `fallback` state once reached |
304
+
305
+ ## Using with SSR / RSC
306
+
307
+ Every hook in this library ships with a `'use client'` directive attached at build time. In React Server Components environments such as Next.js App Router, they are treated as client component boundaries without any additional configuration.
308
+
309
+ `useDeferUntilBreakpoint`, `useDeferUntilIntersected`, and `useDeferUntilRender` depend on browser APIs (`matchMedia`, `IntersectionObserver`, `MutationObserver`, etc.), so their actual state cannot be determined on the server. These hooks accept an `initialCondition` option to specify the value used during SSR (it is treated as `false` if not specified). Similarly, `useDeferUntilWebFontReady` accepts an `initialState` option to specify its initial state during SSR.
310
+
311
+ ## License
312
+
313
+ MIT