@fringeworks/react-defer-rendering 0.8.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 (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +319 -0
  3. package/README.md +319 -0
  4. package/_usePromiseState.cjs +42 -0
  5. package/_usePromiseState.d.cts +11 -0
  6. package/_usePromiseState.d.mts +12 -0
  7. package/_usePromiseState.mjs +40 -0
  8. package/_virtual/_rolldown/runtime.cjs +29 -0
  9. package/constants.cjs +14 -0
  10. package/constants.d.cts +15 -0
  11. package/constants.d.mts +15 -0
  12. package/constants.mjs +12 -0
  13. package/index.cjs +46 -0
  14. package/index.d.cts +31 -0
  15. package/index.d.mts +31 -0
  16. package/index.mjs +32 -0
  17. package/package.json +91 -0
  18. package/types.cjs +0 -0
  19. package/types.d.cts +82 -0
  20. package/types.d.mts +82 -0
  21. package/types.mjs +1 -0
  22. package/useDeferUntilAsyncComplete/index.cjs +5 -0
  23. package/useDeferUntilAsyncComplete/index.d.cts +3 -0
  24. package/useDeferUntilAsyncComplete/index.d.mts +3 -0
  25. package/useDeferUntilAsyncComplete/index.mjs +5 -0
  26. package/useDeferUntilAsyncComplete/types.cjs +0 -0
  27. package/useDeferUntilAsyncComplete/types.d.cts +7 -0
  28. package/useDeferUntilAsyncComplete/types.d.mts +7 -0
  29. package/useDeferUntilAsyncComplete/types.mjs +1 -0
  30. package/useDeferUntilAsyncComplete/useDeferUntilAsyncComplete.cjs +20 -0
  31. package/useDeferUntilAsyncComplete/useDeferUntilAsyncComplete.d.cts +14 -0
  32. package/useDeferUntilAsyncComplete/useDeferUntilAsyncComplete.d.mts +15 -0
  33. package/useDeferUntilAsyncComplete/useDeferUntilAsyncComplete.mjs +20 -0
  34. package/useDeferUntilBreakpoint/index.cjs +5 -0
  35. package/useDeferUntilBreakpoint/index.d.cts +3 -0
  36. package/useDeferUntilBreakpoint/index.d.mts +3 -0
  37. package/useDeferUntilBreakpoint/index.mjs +5 -0
  38. package/useDeferUntilBreakpoint/types.cjs +0 -0
  39. package/useDeferUntilBreakpoint/types.d.cts +20 -0
  40. package/useDeferUntilBreakpoint/types.d.mts +20 -0
  41. package/useDeferUntilBreakpoint/types.mjs +1 -0
  42. package/useDeferUntilBreakpoint/useDeferUntilBreakpoint.cjs +44 -0
  43. package/useDeferUntilBreakpoint/useDeferUntilBreakpoint.d.cts +14 -0
  44. package/useDeferUntilBreakpoint/useDeferUntilBreakpoint.d.mts +15 -0
  45. package/useDeferUntilBreakpoint/useDeferUntilBreakpoint.mjs +42 -0
  46. package/useDeferUntilCallThreshold/index.cjs +5 -0
  47. package/useDeferUntilCallThreshold/index.d.cts +3 -0
  48. package/useDeferUntilCallThreshold/index.d.mts +3 -0
  49. package/useDeferUntilCallThreshold/index.mjs +5 -0
  50. package/useDeferUntilCallThreshold/types.cjs +0 -0
  51. package/useDeferUntilCallThreshold/types.d.cts +23 -0
  52. package/useDeferUntilCallThreshold/types.d.mts +23 -0
  53. package/useDeferUntilCallThreshold/types.mjs +1 -0
  54. package/useDeferUntilCallThreshold/useDeferUntilCallThreshold.cjs +55 -0
  55. package/useDeferUntilCallThreshold/useDeferUntilCallThreshold.d.cts +13 -0
  56. package/useDeferUntilCallThreshold/useDeferUntilCallThreshold.d.mts +14 -0
  57. package/useDeferUntilCallThreshold/useDeferUntilCallThreshold.mjs +55 -0
  58. package/useDeferUntilChange/index.cjs +5 -0
  59. package/useDeferUntilChange/index.d.cts +3 -0
  60. package/useDeferUntilChange/index.d.mts +3 -0
  61. package/useDeferUntilChange/index.mjs +5 -0
  62. package/useDeferUntilChange/types.cjs +0 -0
  63. package/useDeferUntilChange/types.d.cts +7 -0
  64. package/useDeferUntilChange/types.d.mts +7 -0
  65. package/useDeferUntilChange/types.mjs +1 -0
  66. package/useDeferUntilChange/useDeferUntilChange.cjs +30 -0
  67. package/useDeferUntilChange/useDeferUntilChange.d.cts +14 -0
  68. package/useDeferUntilChange/useDeferUntilChange.d.mts +15 -0
  69. package/useDeferUntilChange/useDeferUntilChange.mjs +28 -0
  70. package/useDeferUntilDate/index.cjs +5 -0
  71. package/useDeferUntilDate/index.d.cts +3 -0
  72. package/useDeferUntilDate/index.d.mts +3 -0
  73. package/useDeferUntilDate/index.mjs +5 -0
  74. package/useDeferUntilDate/types.cjs +0 -0
  75. package/useDeferUntilDate/types.d.cts +7 -0
  76. package/useDeferUntilDate/types.d.mts +7 -0
  77. package/useDeferUntilDate/types.mjs +1 -0
  78. package/useDeferUntilDate/useDeferUntilDate.cjs +21 -0
  79. package/useDeferUntilDate/useDeferUntilDate.d.cts +14 -0
  80. package/useDeferUntilDate/useDeferUntilDate.d.mts +15 -0
  81. package/useDeferUntilDate/useDeferUntilDate.mjs +21 -0
  82. package/useDeferUntilIntersected/index.cjs +5 -0
  83. package/useDeferUntilIntersected/index.d.cts +3 -0
  84. package/useDeferUntilIntersected/index.d.mts +3 -0
  85. package/useDeferUntilIntersected/index.mjs +5 -0
  86. package/useDeferUntilIntersected/types.cjs +0 -0
  87. package/useDeferUntilIntersected/types.d.cts +31 -0
  88. package/useDeferUntilIntersected/types.d.mts +31 -0
  89. package/useDeferUntilIntersected/types.mjs +1 -0
  90. package/useDeferUntilIntersected/useDeferUntilIntersected.cjs +60 -0
  91. package/useDeferUntilIntersected/useDeferUntilIntersected.d.cts +14 -0
  92. package/useDeferUntilIntersected/useDeferUntilIntersected.d.mts +15 -0
  93. package/useDeferUntilIntersected/useDeferUntilIntersected.mjs +58 -0
  94. package/useDeferUntilOnReady/index.cjs +5 -0
  95. package/useDeferUntilOnReady/index.d.cts +3 -0
  96. package/useDeferUntilOnReady/index.d.mts +3 -0
  97. package/useDeferUntilOnReady/index.mjs +5 -0
  98. package/useDeferUntilOnReady/types.cjs +0 -0
  99. package/useDeferUntilOnReady/types.d.cts +23 -0
  100. package/useDeferUntilOnReady/types.d.mts +23 -0
  101. package/useDeferUntilOnReady/types.mjs +1 -0
  102. package/useDeferUntilOnReady/useDeferUntilOnReady.cjs +42 -0
  103. package/useDeferUntilOnReady/useDeferUntilOnReady.d.cts +13 -0
  104. package/useDeferUntilOnReady/useDeferUntilOnReady.d.mts +14 -0
  105. package/useDeferUntilOnReady/useDeferUntilOnReady.mjs +40 -0
  106. package/useDeferUntilReady/index.cjs +5 -0
  107. package/useDeferUntilReady/index.d.cts +3 -0
  108. package/useDeferUntilReady/index.d.mts +3 -0
  109. package/useDeferUntilReady/index.mjs +5 -0
  110. package/useDeferUntilReady/types.cjs +0 -0
  111. package/useDeferUntilReady/types.d.cts +7 -0
  112. package/useDeferUntilReady/types.d.mts +7 -0
  113. package/useDeferUntilReady/types.mjs +1 -0
  114. package/useDeferUntilReady/useDeferUntilReady.cjs +60 -0
  115. package/useDeferUntilReady/useDeferUntilReady.d.cts +15 -0
  116. package/useDeferUntilReady/useDeferUntilReady.d.mts +16 -0
  117. package/useDeferUntilReady/useDeferUntilReady.mjs +58 -0
  118. package/useDeferUntilRender/index.cjs +5 -0
  119. package/useDeferUntilRender/index.d.cts +3 -0
  120. package/useDeferUntilRender/index.d.mts +3 -0
  121. package/useDeferUntilRender/index.mjs +5 -0
  122. package/useDeferUntilRender/types.cjs +0 -0
  123. package/useDeferUntilRender/types.d.cts +25 -0
  124. package/useDeferUntilRender/types.d.mts +25 -0
  125. package/useDeferUntilRender/types.mjs +1 -0
  126. package/useDeferUntilRender/useDeferUntilRender.cjs +53 -0
  127. package/useDeferUntilRender/useDeferUntilRender.d.cts +14 -0
  128. package/useDeferUntilRender/useDeferUntilRender.d.mts +15 -0
  129. package/useDeferUntilRender/useDeferUntilRender.mjs +51 -0
  130. package/useDeferUntilResolved/index.cjs +5 -0
  131. package/useDeferUntilResolved/index.d.cts +3 -0
  132. package/useDeferUntilResolved/index.d.mts +3 -0
  133. package/useDeferUntilResolved/index.mjs +5 -0
  134. package/useDeferUntilResolved/types.cjs +0 -0
  135. package/useDeferUntilResolved/types.d.cts +7 -0
  136. package/useDeferUntilResolved/types.d.mts +7 -0
  137. package/useDeferUntilResolved/types.mjs +1 -0
  138. package/useDeferUntilResolved/useDeferUntilResolved.cjs +21 -0
  139. package/useDeferUntilResolved/useDeferUntilResolved.d.cts +14 -0
  140. package/useDeferUntilResolved/useDeferUntilResolved.d.mts +15 -0
  141. package/useDeferUntilResolved/useDeferUntilResolved.mjs +21 -0
  142. package/useDeferUntilScrolled/index.cjs +5 -0
  143. package/useDeferUntilScrolled/index.d.cts +3 -0
  144. package/useDeferUntilScrolled/index.d.mts +3 -0
  145. package/useDeferUntilScrolled/index.mjs +5 -0
  146. package/useDeferUntilScrolled/types.cjs +0 -0
  147. package/useDeferUntilScrolled/types.d.cts +29 -0
  148. package/useDeferUntilScrolled/types.d.mts +29 -0
  149. package/useDeferUntilScrolled/types.mjs +1 -0
  150. package/useDeferUntilScrolled/useDeferUntilScrolled.cjs +94 -0
  151. package/useDeferUntilScrolled/useDeferUntilScrolled.d.cts +14 -0
  152. package/useDeferUntilScrolled/useDeferUntilScrolled.d.mts +15 -0
  153. package/useDeferUntilScrolled/useDeferUntilScrolled.mjs +91 -0
  154. package/useDeferUntilTimeout/index.cjs +5 -0
  155. package/useDeferUntilTimeout/index.d.cts +3 -0
  156. package/useDeferUntilTimeout/index.d.mts +3 -0
  157. package/useDeferUntilTimeout/index.mjs +5 -0
  158. package/useDeferUntilTimeout/types.cjs +0 -0
  159. package/useDeferUntilTimeout/types.d.cts +7 -0
  160. package/useDeferUntilTimeout/types.d.mts +7 -0
  161. package/useDeferUntilTimeout/types.mjs +1 -0
  162. package/useDeferUntilTimeout/useDeferUntilTimeout.cjs +40 -0
  163. package/useDeferUntilTimeout/useDeferUntilTimeout.d.cts +14 -0
  164. package/useDeferUntilTimeout/useDeferUntilTimeout.d.mts +15 -0
  165. package/useDeferUntilTimeout/useDeferUntilTimeout.mjs +37 -0
  166. package/useDeferUntilTrue/index.cjs +5 -0
  167. package/useDeferUntilTrue/index.d.cts +3 -0
  168. package/useDeferUntilTrue/index.d.mts +3 -0
  169. package/useDeferUntilTrue/index.mjs +5 -0
  170. package/useDeferUntilTrue/types.cjs +0 -0
  171. package/useDeferUntilTrue/types.d.cts +7 -0
  172. package/useDeferUntilTrue/types.d.mts +7 -0
  173. package/useDeferUntilTrue/types.mjs +1 -0
  174. package/useDeferUntilTrue/useDeferUntilTrue.cjs +20 -0
  175. package/useDeferUntilTrue/useDeferUntilTrue.d.cts +14 -0
  176. package/useDeferUntilTrue/useDeferUntilTrue.d.mts +15 -0
  177. package/useDeferUntilTrue/useDeferUntilTrue.mjs +20 -0
  178. package/useDeferUntilWebFontReady/index.cjs +5 -0
  179. package/useDeferUntilWebFontReady/index.d.cts +3 -0
  180. package/useDeferUntilWebFontReady/index.d.mts +3 -0
  181. package/useDeferUntilWebFontReady/index.mjs +5 -0
  182. package/useDeferUntilWebFontReady/types.cjs +0 -0
  183. package/useDeferUntilWebFontReady/types.d.cts +49 -0
  184. package/useDeferUntilWebFontReady/types.d.mts +49 -0
  185. package/useDeferUntilWebFontReady/types.mjs +1 -0
  186. package/useDeferUntilWebFontReady/useDeferUntilWebFontReady.cjs +93 -0
  187. package/useDeferUntilWebFontReady/useDeferUntilWebFontReady.d.cts +16 -0
  188. package/useDeferUntilWebFontReady/useDeferUntilWebFontReady.d.mts +17 -0
  189. package/useDeferUntilWebFontReady/useDeferUntilWebFontReady.mjs +90 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mitsuhiro Seino
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,319 @@
1
+ # @fringeworks/react-defer-rendering
2
+
3
+ `@fringeworks/react-defer-rendering` は、指定した条件が満たされるまでコンポーネントの描画を遅延させることに特化したニッチなライブラリです。\
4
+ タイマー、Promise、ブラウザAPI(`matchMedia`、`IntersectionObserver`等)など、様々な条件に対応したフック群を提供します。
5
+
6
+ **[English README is available here](./README.md)**
7
+
8
+ ## インストール
9
+
10
+ ```bash
11
+ npm install @fringeworks/react-defer-rendering
12
+ # または
13
+ pnpm add @fringeworks/react-defer-rendering
14
+ ```
15
+
16
+ ## 使い方
17
+
18
+ 各フックは、対象ノードの描画状態を表す `state`(`'pending' | 'fallback' | 'ready'`)と、状態に応じて描画すべき `node` を返します。
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
+ - 条件を満たしていない間(`pending`)は、`pending` オプションに指定したノードが表示されます
31
+ - 条件を満たすと(`ready`)、第一引数に渡した対象ノードに切り替わります
32
+ - 一部のフックは失敗時の状態(`fallback`)にも対応しています
33
+
34
+ ### 待つ対象が未指定の場合
35
+
36
+ 待つ対象を受け取るフックは、第二引数が`null` / `undefined`の場合、待つものがないとみなして即座に`ready`になります。途中で未指定に変わった場合も`ready`に戻り、未指定から指定に変わった場合は改めて`pending`から待ち始めます。\
37
+ フックは条件付きで呼び出せないため、コンポーネントの機能として遅延描画を任意にしたい場合は、第二引数に`undefined`を渡すことで無効にできます。
38
+
39
+ | フック | 未指定として扱われる第二引数 |
40
+ | ----------------------------------------------------------------------------------------------------- | -------------------------------------- |
41
+ | `useDeferUntilTimeout` / `useDeferUntilDate` / `useDeferUntilResolved` / `useDeferUntilAsyncComplete` | `null` / `undefined` |
42
+ | `useDeferUntilBreakpoint` / `useDeferUntilRender` | `null` / `undefined` / 空文字 |
43
+ | `useDeferUntilWebFontReady` | `null` / `undefined` / 空文字 / 空配列 |
44
+ | `useDeferUntilIntersected` / `useDeferUntilScrolled` | 参照(ref)自体が`null` / `undefined` |
45
+
46
+ `useDeferUntilIntersected` / `useDeferUntilScrolled`では、参照はあるものの`ref.current`が`null`の場合は「要素がまだマウントされていない」とみなし、`pending`のままになります。
47
+
48
+ ```tsx
49
+ function Heading({ fontFamily, children }: Props) {
50
+ // fontFamilyが未指定なら待たずに表示する
51
+ const { node } = useDeferUntilWebFontReady(<h1>{children}</h1>, fontFamily, {
52
+ pending: null,
53
+ });
54
+ return node;
55
+ }
56
+ ```
57
+
58
+ なお、`useDeferUntilTrue`の第二引数は条件そのものであり、`null` / `undefined`は条件を満たしていない(`pending`)として扱われます。
59
+
60
+ ## 基本のフック
61
+
62
+ ### `useDeferUntilReady`
63
+
64
+ `state`(`'pending' | 'fallback' | 'ready'`)を直接指定して描画を制御する、最も基本的なフックです。他のフックはすべてこのフックをベースに実装されています。
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
+ 真偽値の条件が`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
+ ## 時間経過を待つ
88
+
89
+ ### `useDeferUntilTimeout`
90
+
91
+ 指定の時間(ミリ秒)が経過するまで描画を遅延させます。
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
+ 指定の日時になるまで描画を遅延させます。
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
+ ## 非同期処理の完了を待つ
116
+
117
+ ### `useDeferUntilResolved`
118
+
119
+ Promiseが解決(resolve/reject)するまで描画を遅延させます。
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
+ 非同期関数の実行が完了するまで描画を遅延させます。`asyncFn`はマウント後(effect内)に呼び出され、Promiseの生成自体を管理します。SSR時は呼び出されません。\n`asyncFn`の参照が変わるたびに再実行されるため、`useCallback`等でメモ化してください。
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
+ ## 値の変化を待つ
148
+
149
+ ### `useDeferUntilChange`
150
+
151
+ 指定した値が変化するまで描画を遅延させます。値が変化するたびに一時的に`pending`を経由します。
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
+ ## 任意のタイミングで手動制御する
162
+
163
+ ### `useDeferUntilOnReady`
164
+
165
+ 戻り値の`onReady` / `onFallback` / `onPending`を呼び出すことで、任意のタイミングで状態を制御できます。イベントハンドラー等、フック側では検知できない条件に対応する場合に使用します。
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
+ `useDeferUntilOnReady`と同様ですが、各ハンドラーが指定回数呼ばれて初めて状態が切り替わります。
180
+
181
+ ```tsx
182
+ import { useDeferUntilCallThreshold } from '@fringeworks/react-defer-rendering';
183
+
184
+ const { node, onReady } = useDeferUntilCallThreshold(<Gallery />, {
185
+ pending: <Spinner />,
186
+ onReadyCount: 3, // onReadyが3回呼ばれたらreadyにする
187
+ });
188
+ ```
189
+
190
+ ## ブラウザの状態を待つ
191
+
192
+ > **注意(SSR / RSC):** ここで挙げるフックはブラウザAPIに依存するため、サーバー上では実際の状態を判定できません。詳細は[SSR / RSCでの利用について](#ssr--rscでの利用について)を参照してください。
193
+
194
+ ### `useDeferUntilBreakpoint`
195
+
196
+ メディアクエリーが一致するまで描画を遅延させます。
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
+ 基準となる要素がビューポートに入るまで描画を遅延させます。
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
+ 基準となる要素がスクロールによって可視範囲に入るまで描画を遅延させます。
227
+
228
+ ```tsx
229
+ const { node } = useDeferUntilScrolled(<LazyImage />, elementRef, {
230
+ pending: <Placeholder />,
231
+ rootMargin: 100,
232
+ });
233
+ ```
234
+
235
+ ### `useDeferUntilRender`
236
+
237
+ セレクターに一致する要素がDOMに描画されるまで描画を遅延させます。自身の管理外にある要素(サードパーティスクリプトが挿入する要素等)の出現を待つ場合に使用します。
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
+ 指定のWebフォントがすべて利用可能になるまで描画を遅延させます。Webフォントの確認には[`@fringeworks/web-font-observer`](https://www.npmjs.com/package/@fringeworks/web-font-observer)を使用しています。\
250
+ 対象は`@font-face`で定義されたWebフォントです。システムフォントはロードの完了を検知できないため、指定するとタイムアウト後に`fallback`になります。
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
+ 複数のWebフォントを待つ場合は配列で指定します。太さなどを指定する場合はオブジェクトで指定します。配列内の`null` / `undefined`は無視されるため、条件付きでWebフォントを追加できます。\
265
+ 配列やオブジェクトはインラインで記述しても問題ありません(内容が変わらない限り待機はやり直されません)。
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
+ | オプション | 型 | デフォルト | 説明 |
276
+ | --------------- | ---------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------ |
277
+ | `timeout?` | `number` | `3000` | Webフォントが利用可能にならなかった場合に`fallback`とするまでの時間(ミリ秒) |
278
+ | `loader?` | `(signal: AbortSignal) => Promise<void>` | - | Webフォントをロードする関数。完了後に確認を開始します。待つWebフォントが変わった時のみ呼ばれます |
279
+ | `initialState?` | `'pending' \| 'ready' \| 'fallback'` | `'pending'` | SSR時など、読み込み状態を判定できない環境での初期状態 |
280
+
281
+ オブジェクトで指定できる`weight` / `style` / `width` / `text`は`@fringeworks/web-font-observer`と同じです。
282
+
283
+ - 1つでも利用可能にならなかった場合は`fallback`になり、残りの待機は中断されます。
284
+ - 既にロード済みのWebフォントは待たずに`ready`になります(キャッシュ済みの場合にちらつきません)。
285
+ - `fallback`になった後でWebフォントのロードが完了した場合は`ready`になります。`fallback`を維持したい場合は`preserveOnceFallback`を指定してください。
286
+ - `@font-face`の定義は遅延対象のノードの外(グローバルなCSSやレイアウト等)に置いてください。遅延対象の中にあると定義が登録されず、タイムアウトまで待つことになります。
287
+
288
+ ## 共通オプション
289
+
290
+ ### `pending` / `ready` に関するオプション
291
+
292
+ すべてのフックで共通のオプションです。
293
+
294
+ | オプション | 型 | 説明 |
295
+ | -------------------- | ----------- | -------------------------------------------- |
296
+ | `pending?` | `ReactNode` | 条件を待っている間に表示するノード |
297
+ | `pendingDefer?` | `number` | `pending`を表示するまでの遅延時間(ミリ秒) |
298
+ | `readyDefer?` | `number` | 対象ノードを表示するまでの遅延時間(ミリ秒) |
299
+ | `preserveOnceReady?` | `boolean` | 一度`ready`になったら、その状態を保持するか |
300
+
301
+ ### `fallback` に関するオプション
302
+
303
+ `useDeferUntilReady` / `useDeferUntilResolved` / `useDeferUntilAsyncComplete` / `useDeferUntilOnReady` / `useDeferUntilCallThreshold` / `useDeferUntilWebFontReady` は、失敗時の状態として`fallback`にも対応しています。
304
+
305
+ | オプション | 型 | 説明 |
306
+ | ----------------------- | ----------- | ---------------------------------------------- |
307
+ | `fallback?` | `ReactNode` | 失敗時に表示するノード |
308
+ | `fallbackDefer?` | `number` | `fallback`を表示するまでの遅延時間(ミリ秒) |
309
+ | `preserveOnceFallback?` | `boolean` | 一度`fallback`になったら、その状態を保持するか |
310
+
311
+ ## SSR / RSCでの利用について
312
+
313
+ このライブラリの各フックには、ビルド時に`'use client'`ディレクティブが付与されています。Next.jsのApp Router等のReact Server Components環境でも、追加の設定なくクライアントコンポーネントの境界として扱われます。
314
+
315
+ `useDeferUntilBreakpoint` / `useDeferUntilIntersected` / `useDeferUntilScrolled` / `useDeferUntilRender` はブラウザAPI(`matchMedia`、`IntersectionObserver`、`MutationObserver`等)に依存するため、サーバー上では実際の状態を判定できません。これらのフックは`initialCondition`オプションで、SSR時に使用する初期値を指定できます(未指定の場合は`false`として扱われます)。同様に`useDeferUntilWebFontReady`は`initialState`オプションでSSR時の初期状態を指定できます。
316
+
317
+ ## ライセンス
318
+
319
+ MIT
package/README.md ADDED
@@ -0,0 +1,319 @@
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