soseki 0.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.
Files changed (88) hide show
  1. package/.config/_is-debug-mode.ts +9 -0
  2. package/.config/dependency-cruiser.cjs +408 -0
  3. package/.config/env.d.ts +8 -0
  4. package/.config/mise.toml +28 -0
  5. package/.config/navigation-api.d.ts +18 -0
  6. package/.config/tsconfig.build.json +28 -0
  7. package/.config/tsconfig.config.json +21 -0
  8. package/.config/tsconfig.test.json +26 -0
  9. package/.config/tsconfig.web.json +26 -0
  10. package/.config/vitest.client.ts +30 -0
  11. package/package.json +45 -0
  12. package/src/components/action-id.tsx +35 -0
  13. package/src/components/browser-router.tsx +31 -0
  14. package/src/components/hidden-input.tsx +39 -0
  15. package/src/components/outlet.tsx +17 -0
  16. package/src/components/router.tsx +234 -0
  17. package/src/contexts/route-context.ts +21 -0
  18. package/src/contexts/router-context.ts +55 -0
  19. package/src/core/_action-id-registry.ts +11 -0
  20. package/src/core/_capture-stack-trace.ts +12 -0
  21. package/src/core/_compare-route-paths.ts +90 -0
  22. package/src/core/_create-html-form-element-form-form-data.ts +32 -0
  23. package/src/core/_encode-pathname.ts +17 -0
  24. package/src/core/_is-error.ts +16 -0
  25. package/src/core/_is-promise-like.ts +14 -0
  26. package/src/core/_match-route-path.ts +39 -0
  27. package/src/core/_process-routes.ts +56 -0
  28. package/src/core/_singleton.ts +45 -0
  29. package/src/core/_unreachable.ts +22 -0
  30. package/src/core/_use-singleton.ts +24 -0
  31. package/src/core/_valibot.ts +147 -0
  32. package/src/core/_weak-id-registry.ts +125 -0
  33. package/src/core/constants.ts +4 -0
  34. package/src/core/data-map.types.ts +28 -0
  35. package/src/core/data-store.types.ts +25 -0
  36. package/src/core/deferred-promise.ts +408 -0
  37. package/src/core/errors.ts +680 -0
  38. package/src/core/expect-history-entry.ts +95 -0
  39. package/src/core/history-entry-id-schema.ts +27 -0
  40. package/src/core/history-entry-url-schema.ts +35 -0
  41. package/src/core/init-loaders.ts +79 -0
  42. package/src/core/match-routes.ts +91 -0
  43. package/src/core/readonly-form-data.types.ts +63 -0
  44. package/src/core/readonly-url.types.ts +156 -0
  45. package/src/core/redirect-response.ts +36 -0
  46. package/src/core/route-request.ts +92 -0
  47. package/src/core/route.types.ts +351 -0
  48. package/src/core/start-actions.ts +274 -0
  49. package/src/core/start-loaders.ts +254 -0
  50. package/src/core.ts +43 -0
  51. package/src/engines/engine.types.ts +216 -0
  52. package/src/engines/navigation-api-engine.ts +406 -0
  53. package/src/hooks/_use-route-context.ts +19 -0
  54. package/src/hooks/_use-router-context.ts +25 -0
  55. package/src/hooks/use-action-data.ts +37 -0
  56. package/src/hooks/use-loader-data.ts +28 -0
  57. package/src/hooks/use-navigate.ts +64 -0
  58. package/src/hooks/use-params.ts +11 -0
  59. package/src/hooks/use-pathname.ts +10 -0
  60. package/src/hooks/use-submit.ts +111 -0
  61. package/src/soseki.ts +75 -0
  62. package/src/utils/get-action-id.ts +12 -0
  63. package/src/utils/href.ts +17 -0
  64. package/src/utils/redirect.ts +13 -0
  65. package/src/utils/route-index.ts +70 -0
  66. package/src/utils/route-route.ts +111 -0
  67. package/src/utils/set-action-id.ts +14 -0
  68. package/tests/core/_capture-stack-trace.test.ts +46 -0
  69. package/tests/core/_compare-route-paths.test.ts +134 -0
  70. package/tests/core/_encode-pathname.test.ts +77 -0
  71. package/tests/core/_is-error.test.ts +108 -0
  72. package/tests/core/_is-promise-like.test.ts +100 -0
  73. package/tests/core/_match-route-path.test.ts +74 -0
  74. package/tests/core/_process-routes.test.ts +146 -0
  75. package/tests/core/_singleton.test.ts +102 -0
  76. package/tests/core/_unreachable.test.ts +38 -0
  77. package/tests/core/_use-singleton.test.ts +47 -0
  78. package/tests/core/_weak-id-registry.test.ts +137 -0
  79. package/tests/core/deferred-promise.test.ts +218 -0
  80. package/tests/core/expect-history-entry.test.ts +112 -0
  81. package/tests/core/init-loaders.test.ts +172 -0
  82. package/tests/core/match-routes.test.ts +178 -0
  83. package/tests/core/redirect-response.test.ts +36 -0
  84. package/tests/core/route-request.test.ts +93 -0
  85. package/tests/core/start-actions.test.ts +319 -0
  86. package/tests/core/start-loaders.test.ts +276 -0
  87. package/tests/engines/navigation-api-engine.test.ts +162 -0
  88. package/tsconfig.json +7 -0
@@ -0,0 +1,406 @@
1
+ import actionIdRegistry from "../core/_action-id-registry.js";
2
+ import createHtmlFormElementFormFormData from "../core/_create-html-form-element-form-form-data.js";
3
+ import * as v from "../core/_valibot.js";
4
+ import { ACTION_ID_FORM_DATA_NAME } from "../core/constants.js";
5
+ import { NavigationApiNotSupportedError } from "../core/errors.js";
6
+ import expectHistoryEntry from "../core/expect-history-entry.js";
7
+ import HistoryEntryIdSchema, { type HistoryEntryId } from "../core/history-entry-id-schema.js";
8
+ import HistoryEntryUrlSchema from "../core/history-entry-url-schema.js";
9
+ import initLoaders from "../core/init-loaders.js";
10
+ import matchRoutes from "../core/match-routes.js";
11
+ import type { IAction } from "../core/route.types.js";
12
+ import startActions from "../core/start-actions.js";
13
+ import startLoaders from "../core/start-loaders.js";
14
+ import type {
15
+ IEngine,
16
+ InitEngineArgs,
17
+ NavigateArgs,
18
+ RouterState,
19
+ StartEngineArgs,
20
+ SubmitArgs,
21
+ } from "./engine.types.js";
22
+
23
+ /**
24
+ * Navigation API を利用してルーティングの基幹処理を行うエンジンクラスです。
25
+ */
26
+ export default class NavigationApiEngine implements IEngine {
27
+ /**
28
+ * Navigation API の実体です。
29
+ */
30
+ private nav: Navigation;
31
+
32
+ /**
33
+ * すでにイベントリスナーを登録済みのエントリー ID を管理するセットです。
34
+ */
35
+ private subscribedEntryIds: Set<HistoryEntryId>;
36
+
37
+ /**
38
+ * 実行中のナビゲーションをキャンセルするためのコントローラーです。
39
+ */
40
+ private navAbortController: AbortController | null;
41
+
42
+ /**
43
+ * NavigationApiEngine クラスの新しいインスタンスを初期化します。
44
+ */
45
+ public constructor() {
46
+ let nav: Navigation | undefined;
47
+ try {
48
+ nav = navigation;
49
+ } catch {
50
+ try {
51
+ nav = window.navigation;
52
+ } catch {}
53
+ }
54
+ // オブジェクトの存在確認を行い、非対応環境なら例外を投げます。
55
+ if (!(nav && typeof nav === "object")) {
56
+ throw new NavigationApiNotSupportedError();
57
+ }
58
+
59
+ this.nav = nav;
60
+ this.subscribedEntryIds = new Set();
61
+ this.navAbortController = null;
62
+ }
63
+
64
+ /**
65
+ * 現在のロケーションに基づいた初期のルーター状態を生成します。
66
+ *
67
+ * @param args 状態生成に必要なルート定義やデータ保持用のマップです。
68
+ * @returns 生成されたルーター状態、またはマッチしなかった場合は `null` を返します。
69
+ */
70
+ public init(args: InitEngineArgs): RouterState | null {
71
+ const currentEntry = expectHistoryEntry(this.nav.currentEntry);
72
+ if (!currentEntry) {
73
+ return null;
74
+ }
75
+
76
+ const {
77
+ routes,
78
+ getSignal,
79
+ loaderDataStore,
80
+ } = args;
81
+ const currentRoutes = matchRoutes(routes, currentEntry.url.pathname);
82
+ if (!currentRoutes) {
83
+ return null;
84
+ }
85
+
86
+ // 非同期でローダーの初期化を開始します。
87
+ initLoaders({
88
+ entry: currentEntry,
89
+ routes: currentRoutes,
90
+ signal: getSignal(),
91
+ dataStore: loaderDataStore,
92
+ });
93
+
94
+ return {
95
+ entry: currentEntry,
96
+ routes: currentRoutes,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * エンジンの動作を開始し、ナビゲーションイベントの監視を行います。
102
+ *
103
+ * @param args エンジンの開始に必要な設定とデータ管理用のオブジェクトです。
104
+ */
105
+ public start(args: StartEngineArgs): void {
106
+ const {
107
+ routes,
108
+ update,
109
+ getSignal,
110
+ actionDataStore,
111
+ loaderDataStore,
112
+ } = args;
113
+
114
+ /**
115
+ * ナビゲーションが発生した際のハンドラーです。
116
+ *
117
+ * @param event ナビゲーションイベントです。
118
+ */
119
+ const handleNavigate = (event: NavigateEvent): void => {
120
+ // インターセプトできない場合や、ハッシュ変更、ダウンロードリクエストの場合は処理をスキップします。
121
+ // 参照: https://developer.mozilla.org/docs/Web/API/Navigation_API#handling_a_navigation_using_intercept
122
+ if (
123
+ !event.isTrusted
124
+ || !event.canIntercept
125
+ || event.hashChange
126
+ || event.downloadRequest !== null
127
+ ) {
128
+ return;
129
+ }
130
+
131
+ const currentEntry = expectHistoryEntry(this.nav.currentEntry);
132
+ if (!currentEntry) {
133
+ event.intercept({
134
+ async handler() {
135
+ update(null);
136
+ },
137
+ });
138
+ return;
139
+ }
140
+
141
+ const destUrl = v.expect(HistoryEntryUrlSchema(), event.destination.url);
142
+ const destRoutes = matchRoutes(routes, destUrl.pathname);
143
+ if (!destRoutes) {
144
+ event.intercept({
145
+ async handler() {
146
+ update(null);
147
+ },
148
+ });
149
+ return;
150
+ }
151
+
152
+ // 進行中の古い処理をキャンセルし、新しいコントローラーを生成します。
153
+ this.navAbortController?.abort();
154
+ const { signal } = this.navAbortController = new AbortController();
155
+ const { formData } = event;
156
+ const prevEntryInHandler = currentEntry;
157
+ if (formData) {
158
+ const { sourceElement } = event;
159
+ if (sourceElement?.hasAttribute("data-sosekisubmit")) {
160
+ document.body.removeChild(sourceElement);
161
+ }
162
+
163
+ let redirectUrl = new URL(currentEntry.url.href);
164
+ let actionResultMap: ReadonlyMap<IAction, unknown> | undefined;
165
+ event.intercept({
166
+ precommitHandler: async controller => {
167
+ const entry = {
168
+ id: currentEntry.id,
169
+ url: destUrl,
170
+ };
171
+ const waitForComplete = startActions({
172
+ entry,
173
+ routes: destRoutes,
174
+ signal,
175
+ formData,
176
+ dataStore: actionDataStore,
177
+ });
178
+ if (!waitForComplete) {
179
+ return;
180
+ }
181
+
182
+ update();
183
+ const {
184
+ redirect = currentEntry.url.pathname,
185
+ resultMap,
186
+ } = await waitForComplete();
187
+ actionResultMap = resultMap;
188
+ redirectUrl.pathname = redirect;
189
+ controller.redirect(redirect);
190
+ },
191
+ handler: async () => {
192
+ if (!actionResultMap) {
193
+ return;
194
+ }
195
+
196
+ const currentEntry = expectHistoryEntry(this.nav.currentEntry);
197
+ if (!currentEntry) {
198
+ update(null);
199
+ return;
200
+ }
201
+ if (currentEntry.url.href !== redirectUrl.href) {
202
+ return;
203
+ }
204
+
205
+ const currentRoutes = matchRoutes(routes, currentEntry.url.pathname);
206
+ if (!currentRoutes) {
207
+ update(null);
208
+ return;
209
+ }
210
+
211
+ const prevEntry = prevEntryInHandler;
212
+ const prevRoutes = matchRoutes(routes, prevEntry.url.pathname);
213
+ const waitForComplete = startLoaders({
214
+ signal,
215
+ formData,
216
+ prevEntry,
217
+ prevRoutes,
218
+ currentEntry,
219
+ currentRoutes,
220
+ actionResultMap,
221
+ actionDataStore,
222
+ loaderDataStore,
223
+ });
224
+ update({
225
+ entry: currentEntry,
226
+ routes: currentRoutes,
227
+ });
228
+ if (!waitForComplete) {
229
+ return;
230
+ }
231
+
232
+ await waitForComplete();
233
+ },
234
+ });
235
+ } else {
236
+ event.intercept({
237
+ handler: async () => {
238
+ const currentEntry = expectHistoryEntry(this.nav.currentEntry);
239
+ if (!currentEntry) {
240
+ update(null);
241
+ return;
242
+ }
243
+ if (currentEntry.url.href !== destUrl.href) {
244
+ return;
245
+ }
246
+
247
+ const prevEntry = prevEntryInHandler;
248
+ const prevRoutes = matchRoutes(routes, prevEntry.url.pathname);
249
+ const currentRoutes = destRoutes;
250
+ const waitForComplete = startLoaders({
251
+ signal,
252
+ prevEntry,
253
+ prevRoutes,
254
+ currentEntry,
255
+ currentRoutes,
256
+ actionDataStore,
257
+ loaderDataStore,
258
+ });
259
+ update({
260
+ entry: currentEntry,
261
+ routes: currentRoutes,
262
+ });
263
+ await waitForComplete?.();
264
+ },
265
+ });
266
+ }
267
+ };
268
+
269
+ const signal = getSignal();
270
+
271
+ const handleAbort = () => {
272
+ this.navAbortController?.abort();
273
+ this.navAbortController = null;
274
+ };
275
+ signal.addEventListener("abort", handleAbort, { once: true });
276
+
277
+ this.nav.addEventListener("navigate", handleNavigate, { signal });
278
+ // this.nav.addEventListener("navigateerror", console.error, { signal });
279
+
280
+ // 既存のエントリーに対して dispose リスナーを登録し、メモリーを解放します。
281
+ for (const entry of this.nav.entries()) {
282
+ const entryId = v.expect(HistoryEntryIdSchema(), entry.id);
283
+ if (this.subscribedEntryIds.has(entryId)) {
284
+ continue;
285
+ }
286
+
287
+ const handleDispose = () => {
288
+ this.subscribedEntryIds.delete(entryId);
289
+ actionDataStore.delete(entryId);
290
+ loaderDataStore.delete(entryId);
291
+ };
292
+ entry.addEventListener("dispose", handleDispose, { signal });
293
+ this.subscribedEntryIds.add(entryId);
294
+ }
295
+
296
+ /**
297
+ * 現在のエントリーが変更された際のハンドラーです。
298
+ */
299
+ const handleCurrentEntryChange = () => {
300
+ const currentEntry = expectHistoryEntry(this.nav.currentEntry);
301
+ if (!currentEntry) {
302
+ return;
303
+ }
304
+ if (this.subscribedEntryIds.has(currentEntry.id)) {
305
+ return;
306
+ }
307
+
308
+ const handleDispose = () => {
309
+ this.subscribedEntryIds.delete(currentEntry.id);
310
+ actionDataStore.delete(currentEntry.id);
311
+ loaderDataStore.delete(currentEntry.id);
312
+ };
313
+ this.nav.currentEntry!.addEventListener("dispose", handleDispose, { signal });
314
+ this.subscribedEntryIds.add(currentEntry.id);
315
+ };
316
+ this.nav.addEventListener("currententrychange", handleCurrentEntryChange, { signal });
317
+ }
318
+
319
+ /**
320
+ * フォームデータやクエリー パラメーターを送信します。
321
+ *
322
+ * @param args 送信内容と送信先を含む引数です。
323
+ */
324
+ public submit(args: SubmitArgs): void {
325
+ if ("actionId" in args) {
326
+ const {
327
+ target,
328
+ action,
329
+ actionId: actionFunction,
330
+ } = args;
331
+ const form = createHtmlFormElementFormFormData(target);
332
+ const actionId = actionFunction && actionIdRegistry.set(actionFunction);
333
+ if (actionId !== undefined) {
334
+ const input = document.createElement("input");
335
+ input.type = "hidden";
336
+ input.name = ACTION_ID_FORM_DATA_NAME;
337
+ input.value = actionId;
338
+ }
339
+
340
+ form.method = "POST";
341
+ form.action = action;
342
+ form.enctype = "multipart/form-data";
343
+ form.dataset["sosekisubmit"] = "";
344
+ document.body.appendChild(form);
345
+ form.submit();
346
+ } else {
347
+ const {
348
+ target,
349
+ action,
350
+ history,
351
+ } = args;
352
+ const u = new URL("x://y" + action);
353
+ u.search = target.toString();
354
+ this.navigate({
355
+ to: u.href.slice("x://y".length),
356
+ history,
357
+ });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * 指定されたパスへナビゲートします。
363
+ *
364
+ * @param args 遷移先と遷移オプションを含む引数です。
365
+ */
366
+ public navigate(args: NavigateArgs): void {
367
+ if ("delta" in args) {
368
+ const currentEntry = expectHistoryEntry(this.nav.currentEntry);
369
+ if (!currentEntry) {
370
+ return;
371
+ }
372
+
373
+ const { delta } = args;
374
+ const index = currentEntry.index + delta;
375
+ const entry = this.nav.entries().find(e => e.index === index);
376
+ if (!entry) {
377
+ return;
378
+ }
379
+
380
+ this.nav.traverseTo(entry.key);
381
+ } else {
382
+ const {
383
+ to,
384
+ history,
385
+ } = args;
386
+ if (typeof to === "string") {
387
+ this.nav.navigate(to, { history });
388
+ } else {
389
+ const { href } = location;
390
+ const u = new URL(href);
391
+ if (to.pathname !== undefined) {
392
+ u.pathname = to.pathname;
393
+ }
394
+ if (to.search !== undefined) {
395
+ u.search = to.search;
396
+ }
397
+ if (to.hash !== undefined) {
398
+ u.hash = to.hash;
399
+ }
400
+ if (u.href !== href) {
401
+ this.nav.navigate(u.href, { history });
402
+ }
403
+ }
404
+ }
405
+ }
406
+ }
@@ -0,0 +1,19 @@
1
+ import * as React from "react";
2
+ import RouteContext, { type RouteContextValue } from "../contexts/route-context.js";
3
+ import { RouteContextMissingError } from "../core/errors.js";
4
+
5
+ /**
6
+ * ルートに関するコンテキスト情報を取得するためのカスタムフックです。
7
+ *
8
+ * `RouteContext` から現在のコンテキスト値を抽出し、コンテキストが提供されていない場合にはエラーを投げます。
9
+ *
10
+ * @returns 現在のルートコンテキストの値を返します。
11
+ */
12
+ export default function useRouteContext(): RouteContextValue {
13
+ const routeContext = React.use(RouteContext);
14
+ if (!routeContext) {
15
+ throw new RouteContextMissingError();
16
+ }
17
+
18
+ return routeContext;
19
+ }
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import RouterContext, { type RouterRef } from "../contexts/router-context.js";
3
+ import { RouterContextMissingError } from "../core/errors.js";
4
+
5
+ /**
6
+ * ルーターのコンテキストから特定の状態を抽出して取得するためのカスタムフックです。
7
+ *
8
+ * @template TSlice 抽出される状態の型です。
9
+ * @param selector ルーターの参照から必要なデータを抽出するためのセレクター関数です。
10
+ * @returns セレクターによって抽出された状態を返します。
11
+ */
12
+ export default function useRouterContext<TSlice>(
13
+ selector: (router: RouterRef["current"]) => TSlice,
14
+ ): TSlice {
15
+ const routerContext = React.use(RouterContext);
16
+ if (!routerContext) {
17
+ throw new RouterContextMissingError();
18
+ }
19
+
20
+ const {
21
+ routerRef,
22
+ subscribe,
23
+ } = routerContext;
24
+ return React.useSyncExternalStore(subscribe, () => selector(routerRef.current));
25
+ }
@@ -0,0 +1,37 @@
1
+ import type DeferredPromise from "../core/deferred-promise.js";
2
+ import type RedirectResponse from "../core/redirect-response.js";
3
+ import type { IAction } from "../core/route.types.js";
4
+ import useRouterContext from "./_use-router-context.js";
5
+
6
+ /**
7
+ * アクションの実行結果を推論するためのユーティリティー型です。
8
+ *
9
+ * 結果が `RedirectResponse` の場合は `undefined` を返し、それ以外の場合はそのままの型を返します。
10
+ *
11
+ * @template TResult 推論対象となる型です。
12
+ */
13
+ // dprint-ignore
14
+ type $InferResult<TResult> = TResult extends RedirectResponse
15
+ ? undefined
16
+ : TResult;
17
+
18
+ /**
19
+ * 特定のアクションに関連付けられたデータ(実行結果)をルーターのストアから取得するカスタムフックです。
20
+ *
21
+ * 現在の履歴エントリーに対応するアクションデータを返します。
22
+ *
23
+ * @template TAction 対象となるアクションの型です。
24
+ * @param action データを取得したいアクションの定義です。
25
+ * @returns アクションの実行結果を含む `DeferredPromise` を返します。データが存在しない場合は `undefined` を返します。
26
+ */
27
+ export default function useActionData<TAction extends IAction>(
28
+ action: TAction,
29
+ ): DeferredPromise<$InferResult<Awaited<ReturnType<TAction>>>> | undefined {
30
+ return useRouterContext((router): DeferredPromise<any> | undefined => {
31
+ const {
32
+ currentEntry,
33
+ actionDataStore,
34
+ } = router;
35
+ return actionDataStore.get(currentEntry.id)?.get(action);
36
+ });
37
+ }
@@ -0,0 +1,28 @@
1
+ import type DeferredPromise from "../core/deferred-promise.js";
2
+ import { LoaderDataNotFoundError } from "../core/errors.js";
3
+ import type { ILoader } from "../core/route.types.js";
4
+ import useRouterContext from "./_use-router-context.js";
5
+
6
+ /**
7
+ * 指定されたローダーに関連付けられたデータを取得するためのカスタムフックです。
8
+ *
9
+ * @template TLoader 対象となるローダーの型です。
10
+ * @param loader データを取得したいローダーの定義です。
11
+ * @returns ローダーの実行結果を含む `DeferredPromise` を返します。
12
+ */
13
+ export default function useLoaderData<TLoader extends ILoader>(
14
+ loader: TLoader,
15
+ ): DeferredPromise<Awaited<ReturnType<TLoader>>> {
16
+ const loaderData = useRouterContext((router): DeferredPromise<any> | undefined => {
17
+ const {
18
+ currentEntry,
19
+ loaderDataStore,
20
+ } = router;
21
+ return loaderDataStore.get(currentEntry.id)?.get(loader);
22
+ });
23
+ if (!loaderData) {
24
+ throw new LoaderDataNotFoundError(loader);
25
+ }
26
+
27
+ return loaderData;
28
+ }
@@ -0,0 +1,64 @@
1
+ import * as React from "react";
2
+ import type { NavigateTo } from "../engines/engine.types.js";
3
+ import useRouterContext from "./_use-router-context.js";
4
+
5
+ /**
6
+ * 遷移(ナビゲート)を実行する際のオプション設定です。
7
+ */
8
+ export type NavigateOptions = {
9
+ /**
10
+ * 履歴を置き換えるかどうかを指定します。
11
+ *
12
+ * `true` の場合は現在のエントリーを置換し、`false` または未定義の場合は新しいエントリーを追加します。
13
+ */
14
+ readonly replace?: boolean | undefined;
15
+ };
16
+
17
+ /**
18
+ * 指定されたパスへ遷移するための関数インターフェースです。
19
+ */
20
+ export interface INavigate {
21
+ /**
22
+ * ナビゲーションを実行します。
23
+ *
24
+ * @param to 遷移先です。
25
+ * @param options 遷移時のオプション設定です。
26
+ */
27
+ (to: NavigateTo, options?: NavigateOptions | undefined): void;
28
+
29
+ /**
30
+ * ナビゲーションを実行します。
31
+ *
32
+ * @param delta 履歴スタックの相対位置です。
33
+ */
34
+ (delta: number): void;
35
+ }
36
+
37
+ /**
38
+ * プログラムによる遷移(ナビゲート)を行うための関数を取得するカスタムフックです。
39
+ *
40
+ * コンポーネント内でこのフックを使用することで、ボタンのクリックイベントなどで任意のパスへ移動できるようになります。
41
+ *
42
+ * @returns 遷移を実行するための関数である `INavigate` を返します。
43
+ */
44
+ export default function useNavigate(): INavigate {
45
+ const call = useRouterContext(router => router.navigate);
46
+ return React.useCallback(
47
+ function navigate(...args: [NavigateTo, options?: NavigateOptions | undefined] | [number]) {
48
+ if (typeof args[0] === "number") {
49
+ const [delta] = args;
50
+ return call({ delta });
51
+ }
52
+
53
+ const [to, options = {}] = args;
54
+ const { replace = false } = options;
55
+ return call({
56
+ to,
57
+ history: replace
58
+ ? "replace"
59
+ : "push",
60
+ });
61
+ },
62
+ [call],
63
+ );
64
+ }
@@ -0,0 +1,11 @@
1
+ import type { PathParams } from "../core/route.types.js";
2
+ import useRouteContext from "./_use-route-context.js";
3
+
4
+ /**
5
+ * 現在のルートにマッチしたパスパラメーターを取得するためのカスタムフックです。
6
+ *
7
+ * @returns 現在のパスパラメーターを含むオブジェクトを返します。
8
+ */
9
+ export default function useParams<TPath extends string = string>(): PathParams<TPath> {
10
+ return useRouteContext().params as PathParams<TPath>;
11
+ }
@@ -0,0 +1,10 @@
1
+ import useRouterContext from "./_use-router-context.js";
2
+
3
+ /**
4
+ * 現在のルートにおける URL のパスネームを取得するためのカスタムフックです。
5
+ *
6
+ * @returns 現在のルートにおけるパスネームを表す文字列を返します。
7
+ */
8
+ export default function usePathname(): string {
9
+ return useRouterContext(router => router.currentEntry.url.pathname);
10
+ }