@real-router/react 0.27.2 → 0.27.4

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 (75) hide show
  1. package/dist/cjs/{Link-CyDwbrFA.js → Link-D4K4T0Ef.js} +2 -2
  2. package/dist/cjs/{Link-CyDwbrFA.js.map → Link-D4K4T0Ef.js.map} +1 -1
  3. package/dist/cjs/{RouterErrorBoundary-3UhUelqY.d.ts → RouterErrorBoundary-k17kf8Rr.d.ts} +3 -3
  4. package/dist/cjs/{RouterErrorBoundary-3UhUelqY.d.ts.map → RouterErrorBoundary-k17kf8Rr.d.ts.map} +1 -1
  5. package/dist/cjs/{RouterProvider-CPsCmrRw.js → RouterProvider-CAaKExt_.js} +2 -2
  6. package/dist/cjs/RouterProvider-CAaKExt_.js.map +1 -0
  7. package/dist/cjs/{RouterProvider-lgNarto1.d.ts → RouterProvider-xbLo90sc.d.ts} +2 -2
  8. package/dist/cjs/{RouterProvider-lgNarto1.d.ts.map → RouterProvider-xbLo90sc.d.ts.map} +1 -1
  9. package/dist/cjs/index.d.ts +3 -3
  10. package/dist/cjs/index.js +1 -1
  11. package/dist/cjs/index.react-server.d.ts +1 -1
  12. package/dist/cjs/ink.d.ts +2 -2
  13. package/dist/cjs/ink.js +1 -1
  14. package/dist/cjs/legacy.d.ts +3 -3
  15. package/dist/cjs/legacy.js +1 -1
  16. package/dist/cjs/{useRouterTransition-D5nwRENd.d.ts → useRouterTransition-CRFKfwuP.d.ts} +2 -2
  17. package/dist/cjs/{useRouterTransition-D5nwRENd.d.ts.map → useRouterTransition-CRFKfwuP.d.ts.map} +1 -1
  18. package/dist/esm/{Link-uERoM4yk.mjs → Link-PQIMnFd0.mjs} +2 -2
  19. package/dist/esm/{Link-uERoM4yk.mjs.map → Link-PQIMnFd0.mjs.map} +1 -1
  20. package/dist/esm/{RouterErrorBoundary-3UhUelqY.d.mts → RouterErrorBoundary-k17kf8Rr.d.mts} +3 -3
  21. package/dist/esm/{RouterErrorBoundary-3UhUelqY.d.mts.map → RouterErrorBoundary-k17kf8Rr.d.mts.map} +1 -1
  22. package/dist/esm/{RouterProvider-DCchvq4n.d.mts → RouterProvider-CG1cqrjE.d.mts} +2 -2
  23. package/dist/esm/{RouterProvider-DCchvq4n.d.mts.map → RouterProvider-CG1cqrjE.d.mts.map} +1 -1
  24. package/dist/esm/{RouterProvider-DgHypQTJ.mjs → RouterProvider-DhZoi6au.mjs} +2 -2
  25. package/dist/esm/RouterProvider-DhZoi6au.mjs.map +1 -0
  26. package/dist/esm/index.d.mts +3 -3
  27. package/dist/esm/index.mjs +1 -1
  28. package/dist/esm/index.react-server.d.mts +1 -1
  29. package/dist/esm/ink.d.mts +2 -2
  30. package/dist/esm/ink.mjs +1 -1
  31. package/dist/esm/legacy.d.mts +3 -3
  32. package/dist/esm/legacy.mjs +1 -1
  33. package/dist/esm/{useRouterTransition-PCwA_qj1.d.mts → useRouterTransition-C5Ubt7rP.d.mts} +2 -2
  34. package/dist/esm/{useRouterTransition-PCwA_qj1.d.mts.map → useRouterTransition-C5Ubt7rP.d.mts.map} +1 -1
  35. package/package.json +6 -7
  36. package/dist/cjs/RouterProvider-CPsCmrRw.js.map +0 -1
  37. package/dist/esm/RouterProvider-DgHypQTJ.mjs.map +0 -1
  38. package/src/RouterProvider.tsx +0 -148
  39. package/src/components/Await.tsx +0 -46
  40. package/src/components/ClientOnly.tsx +0 -25
  41. package/src/components/HttpStatusCode.tsx +0 -66
  42. package/src/components/HttpStatusProvider.tsx +0 -26
  43. package/src/components/InkLink.tsx +0 -134
  44. package/src/components/InkRouterProvider.tsx +0 -9
  45. package/src/components/Link.tsx +0 -117
  46. package/src/components/RouterErrorBoundary.tsx +0 -69
  47. package/src/components/ServerOnly.tsx +0 -26
  48. package/src/components/Streamed.tsx +0 -30
  49. package/src/components/modern/RouteView/RouteView.tsx +0 -69
  50. package/src/components/modern/RouteView/components.tsx +0 -19
  51. package/src/components/modern/RouteView/helpers.tsx +0 -231
  52. package/src/components/modern/RouteView/index.ts +0 -8
  53. package/src/components/modern/RouteView/types.ts +0 -38
  54. package/src/constants.ts +0 -10
  55. package/src/context.ts +0 -14
  56. package/src/hooks/useDeferred.tsx +0 -34
  57. package/src/hooks/useIsActiveRoute.tsx +0 -47
  58. package/src/hooks/useNavigator.tsx +0 -15
  59. package/src/hooks/useRoute.tsx +0 -32
  60. package/src/hooks/useRouteEnter.tsx +0 -148
  61. package/src/hooks/useRouteExit.tsx +0 -159
  62. package/src/hooks/useRouteNode.tsx +0 -37
  63. package/src/hooks/useRouteUtils.tsx +0 -35
  64. package/src/hooks/useRouter.tsx +0 -15
  65. package/src/hooks/useRouterTransition.tsx +0 -17
  66. package/src/index.react-server.ts +0 -33
  67. package/src/index.ts +0 -61
  68. package/src/ink-types.ts +0 -25
  69. package/src/ink.ts +0 -30
  70. package/src/legacy.ssr.ts +0 -35
  71. package/src/legacy.ts +0 -35
  72. package/src/ssr.react-server.ts +0 -21
  73. package/src/ssr.ts +0 -43
  74. package/src/types.ts +0 -40
  75. package/src/utils/createHttpStatusSink.ts +0 -27
@@ -1,148 +0,0 @@
1
- import { getNavigator } from "@real-router/core";
2
- import { createRouteSource } from "@real-router/sources";
3
- import { useEffect, useMemo, useSyncExternalStore } from "react";
4
-
5
- import { NavigatorContext, RouteContext, RouterContext } from "./context";
6
- import {
7
- createRouteAnnouncer,
8
- createScrollRestoration,
9
- createScrollSpy,
10
- createViewTransitions,
11
- } from "./dom-utils";
12
-
13
- import type { ScrollRestorationOptions, ScrollSpyOptions } from "./dom-utils";
14
- import type { Router } from "@real-router/core";
15
- import type { FC, ReactNode } from "react";
16
-
17
- export interface RouteProviderProps {
18
- router: Router;
19
- children: ReactNode;
20
- announceNavigation?: boolean;
21
- scrollRestoration?: ScrollRestorationOptions;
22
- scrollSpy?: ScrollSpyOptions;
23
- viewTransitions?: boolean;
24
- }
25
-
26
- export const RouterProvider: FC<RouteProviderProps> = ({
27
- router,
28
- children,
29
- announceNavigation,
30
- scrollRestoration,
31
- scrollSpy,
32
- viewTransitions,
33
- }) => {
34
- useEffect(() => {
35
- if (!announceNavigation) {
36
- return;
37
- }
38
-
39
- const announcer = createRouteAnnouncer(router);
40
-
41
- return () => {
42
- announcer.destroy();
43
- };
44
- }, [announceNavigation, router]);
45
-
46
- // Primitive deps so inline `{ mode: "restore" }` doesn't thrash on every
47
- // render. scrollContainer is a getter invoked lazily on every event inside
48
- // the utility — swapping its reference doesn't change the resolved element,
49
- // so we intentionally omit it from deps to keep inline getters stable.
50
- const srMode = scrollRestoration?.mode;
51
- const srAnchor = scrollRestoration?.anchorScrolling;
52
- const srBehavior = scrollRestoration?.behavior;
53
- const srStorageKey = scrollRestoration?.storageKey;
54
- const srEnabled = scrollRestoration !== undefined;
55
-
56
- useEffect(() => {
57
- if (!srEnabled) {
58
- return;
59
- }
60
-
61
- const sr = createScrollRestoration(router, {
62
- mode: srMode,
63
- anchorScrolling: srAnchor,
64
- behavior: srBehavior,
65
- storageKey: srStorageKey,
66
- // srEnabled check above guarantees scrollRestoration is defined.
67
- scrollContainer: scrollRestoration.scrollContainer,
68
- });
69
-
70
- return () => {
71
- sr.destroy();
72
- };
73
- // scrollRestoration (for scrollContainer) omitted — see comment above.
74
- // eslint-disable-next-line @eslint-react/exhaustive-deps
75
- }, [router, srEnabled, srMode, srAnchor, srBehavior, srStorageKey]);
76
-
77
- const spySelector = scrollSpy?.selector;
78
- const spyRootMargin = scrollSpy?.rootMargin;
79
- const spyEnabled =
80
- scrollSpy !== undefined && spySelector !== undefined && spySelector !== "";
81
-
82
- useEffect(() => {
83
- if (!spyEnabled) {
84
- return;
85
- }
86
-
87
- const spy = createScrollSpy(router, {
88
- selector: spySelector,
89
- rootMargin: spyRootMargin,
90
- scrollContainer: scrollSpy.scrollContainer,
91
- });
92
-
93
- return () => {
94
- spy.destroy();
95
- };
96
- // scrollSpy (for scrollContainer) omitted — same rationale as
97
- // scrollRestoration above: getter is invoked lazily inside the utility,
98
- // identity changes don't affect resolution.
99
- // eslint-disable-next-line @eslint-react/exhaustive-deps
100
- }, [router, spyEnabled, spySelector, spyRootMargin]);
101
-
102
- useEffect(() => {
103
- if (!viewTransitions) {
104
- return;
105
- }
106
-
107
- const vt = createViewTransitions(router);
108
-
109
- return () => {
110
- vt.destroy();
111
- };
112
- }, [router, viewTransitions]);
113
-
114
- const navigator = useMemo(() => getNavigator(router), [router]);
115
-
116
- // useSyncExternalStore manages the router subscription lifecycle:
117
- // subscribe connects to router on first listener, unsubscribes on last.
118
- // This is Strict Mode safe — no useEffect cleanup needed.
119
- const store = useMemo(() => createRouteSource(router), [router]);
120
- // Use snapshot reference directly. createRouteSource via stabilizeState
121
- // returns the SAME snapshot reference when route.path is unchanged, so
122
- // useMemo below sees stable deps for idempotent navigations and
123
- // RouteContext consumers do not re-render.
124
- const snapshot = useSyncExternalStore(
125
- store.subscribe,
126
- store.getSnapshot,
127
- store.getSnapshot, // SSR: router returns same state on server and client
128
- );
129
-
130
- const routeContextValue = useMemo(
131
- () => ({
132
- navigator,
133
- route: snapshot.route,
134
- previousRoute: snapshot.previousRoute,
135
- }),
136
- [navigator, snapshot],
137
- );
138
-
139
- return (
140
- <RouterContext.Provider value={router}>
141
- <NavigatorContext.Provider value={navigator}>
142
- <RouteContext.Provider value={routeContextValue}>
143
- {children}
144
- </RouteContext.Provider>
145
- </NavigatorContext.Provider>
146
- </RouterContext.Provider>
147
- );
148
- };
@@ -1,46 +0,0 @@
1
- import { use } from "react";
2
-
3
- import { useDeferred } from "../hooks/useDeferred";
4
-
5
- import type { ReactNode } from "react";
6
-
7
- export interface AwaitProps<T> {
8
- /** Deferred key declared in the loader's `defer({ deferred: { <name>: ... } })`. */
9
- readonly name: string;
10
- /** Render the resolved value. Suspends while pending; throws inside the
11
- * nearest Error Boundary on rejection. */
12
- readonly children: (value: T) => ReactNode;
13
- }
14
-
15
- /**
16
- * Ergonomic wrapper around `useDeferred(name)` + React 19's `use(promise)`.
17
- *
18
- * ```tsx
19
- * <Suspense fallback={<Spinner />}>
20
- * <Await<Review[]> name="reviews">
21
- * {(reviews) => <ReviewList items={reviews} />}
22
- * </Await>
23
- * </Suspense>
24
- * ```
25
- *
26
- * Equivalent to:
27
- *
28
- * ```tsx
29
- * function Inner() {
30
- * const reviews = use(useDeferred<Review[]>("reviews"));
31
- * return <ReviewList items={reviews} />;
32
- * }
33
- * ```
34
- *
35
- * Pick `<Await>` for cross-adapter consistency with the SvelteKit
36
- * `{#await}` / Solid `<Await/>` shape; pick the inline `use(useDeferred(...))`
37
- * form if you prefer one fewer abstraction.
38
- */
39
- export function Await<T = unknown>({
40
- name,
41
- children,
42
- }: AwaitProps<T>): ReactNode {
43
- const value = use(useDeferred<T>(name));
44
-
45
- return children(value);
46
- }
@@ -1,25 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- import type { ReactNode } from "react";
4
-
5
- export interface ClientOnlyProps {
6
- readonly children: ReactNode;
7
- readonly fallback?: ReactNode;
8
- }
9
-
10
- export function ClientOnly({
11
- children,
12
- fallback = null,
13
- }: ClientOnlyProps): ReactNode {
14
- const [mounted, setMounted] = useState(false);
15
-
16
- useEffect(() => {
17
- // SSR/hydration boundary: server emits the fallback branch, client matches
18
- // it on first paint, then this effect flips state to swap in the children.
19
- // The intentional re-render is what makes the markup match across renders.
20
- // eslint-disable-next-line @eslint-react/set-state-in-effect -- intentional post-hydration swap
21
- setMounted(true);
22
- }, []);
23
-
24
- return mounted ? children : fallback;
25
- }
@@ -1,66 +0,0 @@
1
- import { useContext } from "react";
2
-
3
- import { HttpStatusContext } from "./HttpStatusProvider";
4
-
5
- import type { ReactNode } from "react";
6
-
7
- export interface HttpStatusCodeProps {
8
- /** HTTP status to apply to the response. Common values: 404, 410, 451, 503. */
9
- readonly code: number;
10
- }
11
-
12
- /**
13
- * Render-time HTTP status declaration. Mount inside a route component (typical
14
- * use case: a glob `*` route's NotFound page) when the status is decided by
15
- * the rendered tree rather than a loader.
16
- *
17
- * Writes `code` to the nearest `<HttpStatusProvider>`'s sink during render and
18
- * returns `null`. With no provider mounted (the standard client-side case)
19
- * the component is a silent no-op — same component tree hydrates without
20
- * touching the DOM or warning about mismatches.
21
- *
22
- * Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep
23
- * working as before; this component covers render-time decisions only.
24
- *
25
- * Last write wins when several `<HttpStatusCode />` instances mount in the
26
- * same render pass — sink reflects the last component that ran.
27
- *
28
- * ```tsx
29
- * // entry-server.tsx
30
- * import { renderToString } from "react-dom/server";
31
- * import { createHttpStatusSink, HttpStatusProvider } from "@real-router/react/ssr";
32
- *
33
- * const sink = createHttpStatusSink();
34
- * const html = renderToString(
35
- * <HttpStatusProvider sink={sink}>
36
- * <RouterProvider router={router}>
37
- * <App />
38
- * </RouterProvider>
39
- * </HttpStatusProvider>,
40
- * );
41
- * response.status(sink.code ?? 200).send(html);
42
- * ```
43
- *
44
- * **Streaming SSR (`renderToReadableStream`):** the response status MUST be
45
- * sent before the first body byte flushes. If `<HttpStatusCode />` is mounted
46
- * inside a late-resolving `<Suspense>` boundary, the sink write may happen
47
- * AFTER the headers are already on the wire — the override is then lost.
48
- * Either mount the component in the shell (above every `<Suspense>` that
49
- * could delay it) or `await stream.allReady` before reading `sink.code`
50
- * (which forfeits streaming benefits). For non-streaming SSR
51
- * (`renderToString`) there is no such ordering concern.
52
- *
53
- * **Valid `code` range:** Node's `res.end()` throws `Invalid status code` on
54
- * `NaN`, `0`, negative values, or values `> 999` — this surfaces as a 5xx /
55
- * dropped connection, not silent corruption. Pass a real HTTP status integer
56
- * (commonly 4xx/5xx; 100-999 is what Node accepts).
57
- */
58
- export function HttpStatusCode({ code }: HttpStatusCodeProps): ReactNode {
59
- const sink = useContext(HttpStatusContext);
60
-
61
- if (sink) {
62
- sink.code = code;
63
- }
64
-
65
- return null;
66
- }
@@ -1,26 +0,0 @@
1
- import { createContext } from "react";
2
-
3
- import type { HttpStatusSink } from "../utils/createHttpStatusSink";
4
- import type { ReactNode } from "react";
5
-
6
- export const HttpStatusContext = createContext<HttpStatusSink | null>(null);
7
-
8
- export interface HttpStatusProviderProps {
9
- readonly sink: HttpStatusSink;
10
- readonly children: ReactNode;
11
- }
12
-
13
- export function HttpStatusProvider({
14
- sink,
15
- children,
16
- }: HttpStatusProviderProps): ReactNode {
17
- // `<HttpStatusContext.Provider value>` (not the React 19 `<HttpStatusContext value>`
18
- // shorthand) — same component file is exported via `/legacy/ssr` for React 18
19
- // consumers, where the shorthand throws "Element type is invalid: expected
20
- // a string but got: object".
21
- return (
22
- <HttpStatusContext.Provider value={sink}>
23
- {children}
24
- </HttpStatusContext.Provider>
25
- );
26
- }
@@ -1,134 +0,0 @@
1
- import { Text, useFocus, useInput } from "ink";
2
- import { memo } from "react";
3
-
4
- import { EMPTY_OPTIONS, EMPTY_PARAMS } from "../constants";
5
- import { shallowEqual } from "../dom-utils";
6
- import { useIsActiveRoute } from "../hooks/useIsActiveRoute";
7
- import { useRouter } from "../hooks/useRouter";
8
-
9
- import type { InkLinkProps } from "../ink-types";
10
- import type { FC } from "react";
11
-
12
- /**
13
- * Pick the most specific value across focus / active / base priority.
14
- * Focus wins over active; active wins over base. Each layer falls through
15
- * to the next when its value is undefined, matching the original explicit
16
- * if/else cascade for both `color` and `inverse`.
17
- */
18
- function pickPriority<T>(
19
- isFocused: boolean,
20
- isRouteActive: boolean,
21
- focusValue: T | undefined,
22
- activeValue: T | undefined,
23
- baseValue: T | undefined,
24
- ): T | undefined {
25
- if (isFocused) {
26
- return focusValue ?? activeValue ?? baseValue;
27
- }
28
-
29
- if (isRouteActive) {
30
- return activeValue ?? baseValue;
31
- }
32
-
33
- return baseValue;
34
- }
35
-
36
- function areInkLinkPropsEqual(
37
- prev: Readonly<InkLinkProps>,
38
- next: Readonly<InkLinkProps>,
39
- ): boolean {
40
- return (
41
- prev.routeName === next.routeName &&
42
- prev.activeStrict === next.activeStrict &&
43
- prev.ignoreQueryParams === next.ignoreQueryParams &&
44
- prev.color === next.color &&
45
- prev.activeColor === next.activeColor &&
46
- prev.focusColor === next.focusColor &&
47
- prev.inverse === next.inverse &&
48
- prev.activeInverse === next.activeInverse &&
49
- prev.focusInverse === next.focusInverse &&
50
- prev.id === next.id &&
51
- prev.autoFocus === next.autoFocus &&
52
- prev.onSelect === next.onSelect &&
53
- prev.children === next.children &&
54
- shallowEqual(prev.routeParams, next.routeParams) &&
55
- shallowEqual(prev.routeOptions, next.routeOptions)
56
- );
57
- }
58
-
59
- const InkLinkImpl: FC<InkLinkProps> = ({
60
- routeName,
61
- routeParams = EMPTY_PARAMS,
62
- routeOptions = EMPTY_OPTIONS,
63
- activeStrict = false,
64
- ignoreQueryParams = true,
65
- color,
66
- activeColor,
67
- focusColor,
68
- inverse,
69
- activeInverse,
70
- focusInverse,
71
- id,
72
- autoFocus,
73
- onSelect,
74
- children,
75
- }) => {
76
- const router = useRouter();
77
- const { isFocused } = useFocus({
78
- ...(id !== undefined && { id }),
79
- ...(autoFocus !== undefined && { autoFocus }),
80
- });
81
- const isRouteActive = useIsActiveRoute(
82
- routeName,
83
- routeParams,
84
- activeStrict,
85
- ignoreQueryParams,
86
- );
87
-
88
- // No useCallback: `useInput` consumes the handler via its own internal
89
- // ref; a stable identity provides no cache-bail-out benefit here. Same
90
- // reasoning as Link's handleClick.
91
- useInput(
92
- (_input, key) => {
93
- if (key.return) {
94
- onSelect?.();
95
- router.navigate(routeName, routeParams, routeOptions).catch(() => {});
96
- }
97
- },
98
- { isActive: isFocused },
99
- );
100
-
101
- const finalColor = pickPriority(
102
- isFocused,
103
- isRouteActive,
104
- focusColor,
105
- activeColor,
106
- color,
107
- );
108
- const finalInverse = pickPriority(
109
- isFocused,
110
- isRouteActive,
111
- focusInverse,
112
- activeInverse,
113
- inverse,
114
- );
115
-
116
- const textProps: { color?: string; inverse?: boolean } = {};
117
-
118
- if (finalColor !== undefined) {
119
- textProps.color = finalColor;
120
- }
121
-
122
- if (finalInverse !== undefined) {
123
- textProps.inverse = finalInverse;
124
- }
125
-
126
- return <Text {...textProps}>{children}</Text>;
127
- };
128
-
129
- export const InkLink: FC<InkLinkProps> = memo(
130
- InkLinkImpl,
131
- areInkLinkPropsEqual,
132
- );
133
-
134
- InkLink.displayName = "InkLink";
@@ -1,9 +0,0 @@
1
- import { RouterProvider } from "../RouterProvider";
2
-
3
- import type { InkRouterProviderProps } from "../ink-types";
4
- import type { FC } from "react";
5
-
6
- export const InkRouterProvider: FC<InkRouterProviderProps> = ({
7
- router,
8
- children,
9
- }) => <RouterProvider router={router}>{children}</RouterProvider>;
@@ -1,117 +0,0 @@
1
- import { memo, useMemo } from "react";
2
-
3
- import { EMPTY_PARAMS, EMPTY_OPTIONS } from "../constants";
4
- import {
5
- shouldNavigate,
6
- buildHref,
7
- buildActiveClassName,
8
- navigateWithHash,
9
- shallowEqual,
10
- } from "../dom-utils";
11
- import { useIsActiveRoute } from "../hooks/useIsActiveRoute";
12
- import { useRouter } from "../hooks/useRouter";
13
-
14
- import type { LinkProps } from "../types";
15
- import type { FC, MouseEvent } from "react";
16
-
17
- function areLinkPropsEqual(
18
- prev: Readonly<LinkProps>,
19
- next: Readonly<LinkProps>,
20
- ): boolean {
21
- return (
22
- prev.routeName === next.routeName &&
23
- prev.className === next.className &&
24
- prev.activeClassName === next.activeClassName &&
25
- prev.activeStrict === next.activeStrict &&
26
- prev.ignoreQueryParams === next.ignoreQueryParams &&
27
- prev.onClick === next.onClick &&
28
- prev.target === next.target &&
29
- prev.style === next.style &&
30
- prev.children === next.children &&
31
- prev.hash === next.hash &&
32
- shallowEqual(prev.routeParams, next.routeParams) &&
33
- shallowEqual(prev.routeOptions, next.routeOptions)
34
- );
35
- }
36
-
37
- const LinkImpl: FC<LinkProps> = ({
38
- routeName,
39
- routeParams = EMPTY_PARAMS,
40
- routeOptions = EMPTY_OPTIONS,
41
- className,
42
- activeClassName = "active",
43
- activeStrict = false,
44
- ignoreQueryParams = true,
45
- hash,
46
- onClick,
47
- target,
48
- children,
49
- ...props
50
- }) => {
51
- // memo + areLinkPropsEqual guarantees that on bail-out the component does
52
- // not render; on render, routeParams/routeOptions either changed reference
53
- // (true change) or comparator failed (e.g., BigInt fallback to identity),
54
- // so they're safe to use directly in hook deps.
55
-
56
- const router = useRouter();
57
-
58
- // When `hash` prop is set, active state requires both route AND hash to
59
- // match (#532). Without this, three tab links sharing routeName="settings"
60
- // would all be marked active by route-name alone, defeating tab semantics.
61
- const isActive = useIsActiveRoute(
62
- routeName,
63
- routeParams,
64
- activeStrict,
65
- ignoreQueryParams,
66
- hash,
67
- );
68
-
69
- // No useMemo: outer memo()+shallowEqual prevents Link re-render unless a
70
- // prop actually changed. When this body runs, either `hash` differs from
71
- // last render or another prop changed — in both cases the `{ hash }` alloc
72
- // is unavoidable. The useMemo wrapper added one closure + deps slot per
73
- // render without saving an allocation.
74
- const hashOption = hash === undefined ? undefined : { hash };
75
- const href = buildHref(router, routeName, routeParams, hashOption);
76
-
77
- // useCallback was wasteful: 7 deps recreated the closure on every meaningful
78
- // render anyway, and `<a onClick>` does not benefit from a stable function
79
- // identity (no child-memo-bail-out chain past it). Inline arrow function is
80
- // what React Compiler emits automatically for this shape.
81
- const handleClick = (evt: MouseEvent<HTMLAnchorElement>) => {
82
- if (onClick) {
83
- onClick(evt);
84
-
85
- if (evt.defaultPrevented) {
86
- return;
87
- }
88
- }
89
-
90
- if (!shouldNavigate(evt.nativeEvent) || target === "_blank") {
91
- return;
92
- }
93
-
94
- evt.preventDefault();
95
- navigateWithHash(router, routeName, routeParams, hash, routeOptions).catch(
96
- () => {},
97
- );
98
- };
99
-
100
- // Memoize the joined class string. parseTokens + Set + join on every render
101
- // adds up on pages with N Links navigating frequently; deps cover every
102
- // input the function reads so cache invalidation is exact.
103
- const finalClassName = useMemo(
104
- () => buildActiveClassName(isActive, activeClassName, className),
105
- [isActive, activeClassName, className],
106
- );
107
-
108
- return (
109
- <a {...props} href={href} className={finalClassName} onClick={handleClick}>
110
- {children}
111
- </a>
112
- );
113
- };
114
-
115
- export const Link: FC<LinkProps> = memo(LinkImpl, areLinkPropsEqual);
116
-
117
- Link.displayName = "Link";
@@ -1,69 +0,0 @@
1
- import { createDismissableError } from "@real-router/sources";
2
- import {
3
- useEffect,
4
- useLayoutEffect,
5
- useMemo,
6
- useRef,
7
- useSyncExternalStore,
8
- } from "react";
9
-
10
- import { useRouter } from "../hooks/useRouter";
11
-
12
- import type { RouterError, State } from "@real-router/core";
13
- import type { ReactElement, ReactNode } from "react";
14
-
15
- export interface RouterErrorBoundaryProps {
16
- readonly children: ReactNode;
17
- readonly fallback: (error: RouterError, resetError: () => void) => ReactNode;
18
- readonly onError?: (
19
- error: RouterError,
20
- toRoute: State | null,
21
- fromRoute: State | null,
22
- ) => void;
23
- }
24
-
25
- export function RouterErrorBoundary({
26
- children,
27
- fallback,
28
- onError,
29
- }: RouterErrorBoundaryProps): ReactElement {
30
- const router = useRouter();
31
- // Per-router cached in @real-router/sources — the WeakMap lookup is cheap,
32
- // but useMemo avoids it entirely on stable-router re-renders (the common
33
- // case for boundaries mounted in app shells).
34
- const store = useMemo(() => createDismissableError(router), [router]);
35
- const snapshot = useSyncExternalStore(
36
- store.subscribe,
37
- store.getSnapshot,
38
- store.getSnapshot,
39
- );
40
-
41
- const onErrorRef = useRef(onError);
42
-
43
- // "Latest ref" pattern shared with useRouteEnter/useRouteExit: sync the
44
- // callback to the ref via useLayoutEffect (synchronous, post-render,
45
- // pre-paint) so the snapshot effect below reads the freshest callback
46
- // without listing `onError` as a dep — and StrictMode's double-effect
47
- // pass writes the same value twice harmlessly.
48
- useLayoutEffect(() => {
49
- onErrorRef.current = onError;
50
- });
51
-
52
- useEffect(() => {
53
- if (snapshot.error) {
54
- onErrorRef.current?.(
55
- snapshot.error,
56
- snapshot.toRoute,
57
- snapshot.fromRoute,
58
- );
59
- }
60
- // eslint-disable-next-line @eslint-react/exhaustive-deps -- onError tracked via ref, snapshot fields accessed inside callback
61
- }, [snapshot.version]);
62
-
63
- return (
64
- <>
65
- {children}
66
- {snapshot.error ? fallback(snapshot.error, snapshot.resetError) : null}
67
- </>
68
- );
69
- }
@@ -1,26 +0,0 @@
1
- import { useEffect, useState } from "react";
2
-
3
- import type { ReactNode } from "react";
4
-
5
- export interface ServerOnlyProps {
6
- readonly children: ReactNode;
7
- readonly fallback?: ReactNode;
8
- }
9
-
10
- export function ServerOnly({
11
- children,
12
- fallback = null,
13
- }: ServerOnlyProps): ReactNode {
14
- const [mounted, setMounted] = useState(false);
15
-
16
- useEffect(() => {
17
- // SSR/hydration boundary: server emits the children branch, client matches
18
- // it on first paint, then this effect flips state to swap in the fallback
19
- // (or hide entirely). The intentional re-render keeps markup consistent
20
- // across renders.
21
- // eslint-disable-next-line @eslint-react/set-state-in-effect -- intentional post-hydration swap
22
- setMounted(true);
23
- }, []);
24
-
25
- return mounted ? fallback : children;
26
- }