@alepha/react 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alepha/react",
3
3
  "description": "Build server-side rendered (SSR) or single-page React applications.",
4
- "version": "0.9.4",
4
+ "version": "0.9.5",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.0.0"
@@ -17,21 +17,22 @@
17
17
  "src"
18
18
  ],
19
19
  "dependencies": {
20
- "@alepha/core": "0.9.4",
21
- "@alepha/datetime": "0.9.4",
22
- "@alepha/logger": "0.9.4",
23
- "@alepha/router": "0.9.4",
24
- "@alepha/server": "0.9.4",
25
- "@alepha/server-cache": "0.9.4",
26
- "@alepha/server-links": "0.9.4",
27
- "@alepha/server-static": "0.9.4",
20
+ "@alepha/core": "0.9.5",
21
+ "@alepha/datetime": "0.9.5",
22
+ "@alepha/logger": "0.9.5",
23
+ "@alepha/router": "0.9.5",
24
+ "@alepha/server": "0.9.5",
25
+ "@alepha/server-cache": "0.9.5",
26
+ "@alepha/server-links": "0.9.5",
27
+ "@alepha/server-static": "0.9.5",
28
28
  "react-dom": "^19.1.1"
29
29
  },
30
30
  "devDependencies": {
31
- "@types/react": "^19.1.10",
32
- "@types/react-dom": "^19.1.7",
31
+ "@biomejs/biome": "^2.2.4",
32
+ "@types/react": "^19.1.13",
33
+ "@types/react-dom": "^19.1.9",
33
34
  "react": "^19.1.1",
34
- "tsdown": "^0.14.1",
35
+ "tsdown": "^0.15.1",
35
36
  "typescript": "^5.9.2",
36
37
  "vitest": "^3.2.4"
37
38
  },
@@ -41,6 +42,7 @@
41
42
  "scripts": {
42
43
  "check": "tsc",
43
44
  "test": "vitest run",
45
+ "lint": "biome check --write --unsafe",
44
46
  "build": "tsdown -c ../../tsdown.config.ts"
45
47
  },
46
48
  "homepage": "https://github.com/feunard/alepha",
@@ -1,18 +1,15 @@
1
- import type React from "react";
2
1
  import type { AnchorHTMLAttributes } from "react";
3
2
  import { useRouter } from "../hooks/useRouter.ts";
4
3
 
5
4
  export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
6
- to: string;
7
- children?: React.ReactNode;
5
+ href: string;
8
6
  }
9
7
 
10
8
  const Link = (props: LinkProps) => {
11
9
  const router = useRouter();
12
- const { to, ...anchorProps } = props;
13
10
 
14
11
  return (
15
- <a {...router.anchor(to)} {...anchorProps}>
12
+ <a {...props} {...router.anchor(props.href)}>
16
13
  {props.children}
17
14
  </a>
18
15
  );
@@ -1,13 +1,15 @@
1
- import type { ReactNode } from "react";
2
- import { useContext, useState } from "react";
1
+ import { memo, type ReactNode, use, useRef, useState } from "react";
3
2
  import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
3
+ import type { PageAnimation } from "../descriptors/$page.ts";
4
4
  import { Redirection } from "../errors/Redirection.ts";
5
- import { useAlepha } from "../hooks/useAlepha.ts";
6
5
  import { useRouterEvents } from "../hooks/useRouterEvents.ts";
6
+ import { useRouterState } from "../hooks/useRouterState.ts";
7
+ import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
7
8
  import ErrorBoundary from "./ErrorBoundary.tsx";
8
9
 
9
10
  export interface NestedViewProps {
10
11
  children?: ReactNode;
12
+ errorBoundary?: false | ((error: Error) => ReactNode);
11
13
  }
12
14
 
13
15
  /**
@@ -17,7 +19,7 @@ export interface NestedViewProps {
17
19
  *
18
20
  * @example
19
21
  * ```tsx
20
- * import { NestedView } from "@alepha/react";
22
+ * import { NestedView } from "alepha/react";
21
23
  *
22
24
  * class App {
23
25
  * parent = $page({
@@ -32,30 +34,115 @@ export interface NestedViewProps {
32
34
  * ```
33
35
  */
34
36
  const NestedView = (props: NestedViewProps) => {
35
- const layer = useContext(RouterLayerContext);
36
- const index = layer?.index ?? 0;
37
- const alepha = useAlepha();
38
- const state = alepha.state("react.router.state");
39
- if (!state) {
40
- throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
41
- }
37
+ const index = use(RouterLayerContext)?.index ?? 0;
38
+ const state = useRouterState();
42
39
 
43
40
  const [view, setView] = useState<ReactNode | undefined>(
44
41
  state.layers[index]?.element,
45
42
  );
46
43
 
44
+ const [animation, setAnimation] = useState("");
45
+ const animationExitDuration = useRef<number>(0);
46
+ const animationExitNow = useRef<number>(0);
47
+
47
48
  useRouterEvents(
48
49
  {
49
- onEnd: ({ state }) => {
50
- if (!state.layers[index]?.cache) {
51
- setView(state.layers[index]?.element);
50
+ onBegin: async ({ previous, state }) => {
51
+ // --------- Animations Begin ---------
52
+ const layer = previous.layers[index];
53
+ if (`${state.url.pathname}/`.startsWith(`${layer?.path}/`)) {
54
+ return;
55
+ }
56
+
57
+ const animationExit = parseAnimation(
58
+ layer.route?.animation,
59
+ state,
60
+ "exit",
61
+ );
62
+
63
+ if (animationExit) {
64
+ const duration = animationExit.duration || 200;
65
+ animationExitNow.current = Date.now();
66
+ animationExitDuration.current = duration;
67
+ setAnimation(animationExit.animation);
68
+ } else {
69
+ animationExitNow.current = 0;
70
+ animationExitDuration.current = 0;
71
+ setAnimation("");
72
+ }
73
+ // --------- Animations End ---------
74
+ },
75
+ onEnd: async ({ state }) => {
76
+ const layer = state.layers[index];
77
+
78
+ // --------- Animations Begin ---------
79
+ if (animationExitNow.current) {
80
+ const duration = animationExitDuration.current;
81
+ const diff = Date.now() - animationExitNow.current;
82
+ if (diff < duration) {
83
+ await new Promise((resolve) =>
84
+ setTimeout(resolve, duration - diff),
85
+ );
86
+ }
87
+ }
88
+ // --------- Animations End ---------
89
+
90
+ if (!layer?.cache) {
91
+ setView(layer?.element);
92
+
93
+ // --------- Animations Begin ---------
94
+ const animationEnter = parseAnimation(
95
+ layer?.route?.animation,
96
+ state,
97
+ "enter",
98
+ );
99
+
100
+ if (animationEnter) {
101
+ setAnimation(animationEnter.animation);
102
+ } else {
103
+ setAnimation("");
104
+ }
105
+ // --------- Animations End ---------
52
106
  }
53
107
  },
54
108
  },
55
109
  [],
56
110
  );
57
111
 
58
- const element = view ?? props.children ?? null;
112
+ let element = view ?? props.children ?? null;
113
+
114
+ // --------- Animations Begin ---------
115
+ if (animation) {
116
+ element = (
117
+ <div
118
+ style={{
119
+ display: "flex",
120
+ flex: 1,
121
+ height: "100%",
122
+ width: "100%",
123
+ position: "relative",
124
+ overflow: "hidden",
125
+ }}
126
+ >
127
+ <div
128
+ style={{ height: "100%", width: "100%", display: "flex", animation }}
129
+ >
130
+ {element}
131
+ </div>
132
+ </div>
133
+ );
134
+ }
135
+ // --------- Animations End ---------
136
+
137
+ if (props.errorBoundary === false) {
138
+ return <>{element}</>;
139
+ }
140
+
141
+ if (props.errorBoundary) {
142
+ return (
143
+ <ErrorBoundary fallback={props.errorBoundary}>{element}</ErrorBoundary>
144
+ );
145
+ }
59
146
 
60
147
  return (
61
148
  <ErrorBoundary
@@ -72,4 +159,60 @@ const NestedView = (props: NestedViewProps) => {
72
159
  );
73
160
  };
74
161
 
75
- export default NestedView;
162
+ export default memo(NestedView);
163
+
164
+ function parseAnimation(
165
+ animationLike: PageAnimation | undefined,
166
+ state: ReactRouterState,
167
+ type: "enter" | "exit" = "enter",
168
+ ):
169
+ | {
170
+ duration: number;
171
+ animation: string;
172
+ }
173
+ | undefined {
174
+ if (!animationLike) {
175
+ return undefined;
176
+ }
177
+
178
+ const DEFAULT_DURATION = 300;
179
+
180
+ const animation =
181
+ typeof animationLike === "function" ? animationLike(state) : animationLike;
182
+
183
+ if (typeof animation === "string") {
184
+ if (type === "exit") {
185
+ return;
186
+ }
187
+ return {
188
+ duration: DEFAULT_DURATION,
189
+ animation: `${DEFAULT_DURATION}ms ${animation}`,
190
+ };
191
+ }
192
+
193
+ if (typeof animation === "object") {
194
+ const anim = animation[type];
195
+ const duration =
196
+ typeof anim === "object"
197
+ ? (anim.duration ?? DEFAULT_DURATION)
198
+ : DEFAULT_DURATION;
199
+ const name = typeof anim === "object" ? anim.name : anim;
200
+
201
+ if (type === "exit") {
202
+ const timing = typeof anim === "object" ? (anim.timing ?? "") : "";
203
+ return {
204
+ duration,
205
+ animation: `${duration}ms ${timing} ${name}`,
206
+ };
207
+ }
208
+
209
+ const timing = typeof anim === "object" ? (anim.timing ?? "") : "";
210
+
211
+ return {
212
+ duration,
213
+ animation: `${duration}ms ${timing} ${name}`,
214
+ };
215
+ }
216
+
217
+ return undefined;
218
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AlephaError,
2
3
  type Async,
3
4
  createDescriptor,
4
5
  Descriptor,
@@ -179,6 +180,50 @@ export interface PageDescriptorOptions<
179
180
  * Called when user leaves the page. (browser only)
180
181
  */
181
182
  onLeave?: () => void;
183
+
184
+ /**
185
+ * @experimental
186
+ *
187
+ * Add a css animation when the page is loaded or unloaded.
188
+ * It uses CSS animations, so you need to define the keyframes in your CSS.
189
+ *
190
+ * @example Simple animation name
191
+ * ```ts
192
+ * animation: "fadeIn"
193
+ * ```
194
+ *
195
+ * CSS example:
196
+ * ```css
197
+ * @keyframes fadeIn {
198
+ * from { opacity: 0; }
199
+ * to { opacity: 1; }
200
+ * }
201
+ * ```
202
+ *
203
+ * @example Detailed animation
204
+ * ```ts
205
+ * animation: {
206
+ * enter: { name: "fadeIn", duration: 300 },
207
+ * exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
208
+ * }
209
+ * ```
210
+ *
211
+ * @example Only exit animation
212
+ * ```ts
213
+ * animation: {
214
+ * exit: "fadeOut"
215
+ * }
216
+ * ```
217
+ *
218
+ * @example With custom timing function
219
+ * ```ts
220
+ * animation: {
221
+ * enter: { name: "fadeIn", duration: 300, timing: "cubic-bezier(0.4, 0, 0.2, 1)" },
222
+ * exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
223
+ * }
224
+ * ```
225
+ */
226
+ animation?: PageAnimation;
182
227
  }
183
228
 
184
229
  export type ErrorHandler = (
@@ -211,7 +256,18 @@ export class PageDescriptor<
211
256
  public async render(
212
257
  options?: PageDescriptorRenderOptions,
213
258
  ): Promise<PageDescriptorRenderResult> {
214
- throw new Error("render method is not implemented in this environment");
259
+ throw new AlephaError(
260
+ "render() method is not implemented in this environment",
261
+ );
262
+ }
263
+
264
+ public async fetch(options?: PageDescriptorRenderOptions): Promise<{
265
+ html: string;
266
+ response: Response;
267
+ }> {
268
+ throw new AlephaError(
269
+ "fetch() method is not implemented in this environment",
270
+ );
215
271
  }
216
272
 
217
273
  public match(url: string): boolean {
@@ -241,6 +297,13 @@ export type TPropsParentDefault = {};
241
297
  export interface PageDescriptorRenderOptions {
242
298
  params?: Record<string, string>;
243
299
  query?: Record<string, string>;
300
+
301
+ /**
302
+ * If true, the HTML layout will be included in the response.
303
+ * If false, only the page content will be returned.
304
+ *
305
+ * @default true
306
+ */
244
307
  html?: boolean;
245
308
  hydration?: boolean;
246
309
  }
@@ -248,6 +311,7 @@ export interface PageDescriptorRenderOptions {
248
311
  export interface PageDescriptorRenderResult {
249
312
  html: string;
250
313
  state: ReactRouterState;
314
+ redirect?: string;
251
315
  }
252
316
 
253
317
  export interface PageRequestConfig<
@@ -268,3 +332,22 @@ export type PageResolve<
268
332
  > = PageRequestConfig<TConfig> &
269
333
  TPropsParent &
270
334
  Omit<ReactRouterState, "layers" | "onError">;
335
+
336
+ export type PageAnimation =
337
+ | PageAnimationObject
338
+ | ((state: ReactRouterState) => PageAnimationObject | undefined);
339
+
340
+ type PageAnimationObject =
341
+ | CssAnimationName
342
+ | {
343
+ enter?: CssAnimation | CssAnimationName;
344
+ exit?: CssAnimation | CssAnimationName;
345
+ };
346
+
347
+ type CssAnimationName = string;
348
+
349
+ type CssAnimation = {
350
+ name: string;
351
+ duration?: number;
352
+ timing?: string;
353
+ };
@@ -51,5 +51,4 @@ export interface UseActiveHook {
51
51
  isActive: boolean;
52
52
  anchorProps: AnchorProps;
53
53
  isPending: boolean;
54
- name?: string;
55
54
  }
@@ -1,15 +1,23 @@
1
+ import type { Hooks } from "@alepha/core";
1
2
  import { useEffect } from "react";
2
- import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
3
3
  import { useAlepha } from "./useAlepha.ts";
4
4
 
5
+ type Hook<T extends keyof Hooks> =
6
+ | ((ev: Hooks[T]) => void)
7
+ | {
8
+ priority?: "first" | "last";
9
+ callback: (ev: Hooks[T]) => void;
10
+ };
11
+
5
12
  /**
6
13
  * Subscribe to various router events.
7
14
  */
8
15
  export const useRouterEvents = (
9
16
  opts: {
10
- onBegin?: (ev: { state: ReactRouterState }) => void;
11
- onEnd?: (ev: { state: ReactRouterState }) => void;
12
- onError?: (ev: { state: ReactRouterState; error: Error }) => void;
17
+ onBegin?: Hook<"react:transition:begin">;
18
+ onError?: Hook<"react:transition:error">;
19
+ onEnd?: Hook<"react:transition:end">;
20
+ onSuccess?: Hook<"react:transition:success">;
13
21
  } = {},
14
22
  deps: any[] = [],
15
23
  ) => {
@@ -20,33 +28,33 @@ export const useRouterEvents = (
20
28
  return;
21
29
  }
22
30
 
31
+ const cb = <T extends keyof Hooks>(callback: Hook<T>) => {
32
+ if (typeof callback === "function") {
33
+ return { callback };
34
+ }
35
+ return callback;
36
+ };
37
+
23
38
  const subs: Function[] = [];
24
39
  const onBegin = opts.onBegin;
25
40
  const onEnd = opts.onEnd;
26
41
  const onError = opts.onError;
42
+ const onSuccess = opts.onSuccess;
27
43
 
28
44
  if (onBegin) {
29
- subs.push(
30
- alepha.on("react:transition:begin", {
31
- callback: onBegin,
32
- }),
33
- );
45
+ subs.push(alepha.on("react:transition:begin", cb(onBegin)));
34
46
  }
35
47
 
36
48
  if (onEnd) {
37
- subs.push(
38
- alepha.on("react:transition:end", {
39
- callback: onEnd,
40
- }),
41
- );
49
+ subs.push(alepha.on("react:transition:end", cb(onEnd)));
42
50
  }
43
51
 
44
52
  if (onError) {
45
- subs.push(
46
- alepha.on("react:transition:error", {
47
- callback: onError,
48
- }),
49
- );
53
+ subs.push(alepha.on("react:transition:error", cb(onError)));
54
+ }
55
+
56
+ if (onSuccess) {
57
+ subs.push(alepha.on("react:transition:success", cb(onSuccess)));
50
58
  }
51
59
 
52
60
  return () => {
@@ -3,6 +3,7 @@ import { AlephaServer } from "@alepha/server";
3
3
  import { AlephaServerLinks } from "@alepha/server-links";
4
4
  import { $page } from "./descriptors/$page.ts";
5
5
  import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
6
+ import { ReactBrowserRendererProvider } from "./providers/ReactBrowserRendererProvider.ts";
6
7
  import { ReactBrowserRouterProvider } from "./providers/ReactBrowserRouterProvider.ts";
7
8
  import { ReactPageProvider } from "./providers/ReactPageProvider.ts";
8
9
  import { ReactRouter } from "./services/ReactRouter.ts";
@@ -24,6 +25,7 @@ export const AlephaReact = $module({
24
25
  ReactBrowserRouterProvider,
25
26
  ReactBrowserProvider,
26
27
  ReactRouter,
28
+ ReactBrowserRendererProvider,
27
29
  ],
28
30
  register: (alepha) =>
29
31
  alepha
@@ -32,5 +34,6 @@ export const AlephaReact = $module({
32
34
  .with(ReactPageProvider)
33
35
  .with(ReactBrowserProvider)
34
36
  .with(ReactBrowserRouterProvider)
37
+ .with(ReactBrowserRendererProvider)
35
38
  .with(ReactRouter),
36
39
  });
package/src/index.ts CHANGED
@@ -2,7 +2,8 @@ import { $module } from "@alepha/core";
2
2
  import { AlephaServer, type ServerRequest } from "@alepha/server";
3
3
  import { AlephaServerCache } from "@alepha/server-cache";
4
4
  import { AlephaServerLinks } from "@alepha/server-links";
5
- import { $page } from "./descriptors/$page.ts";
5
+ import type { ReactNode } from "react";
6
+ import { $page, type PageAnimation } from "./descriptors/$page.ts";
6
7
  import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
7
8
  import {
8
9
  ReactPageProvider,
@@ -37,11 +38,15 @@ declare module "@alepha/core" {
37
38
  };
38
39
  // -----------------------------------------------------------------------------------------------------------------
39
40
  "react:browser:render": {
41
+ root: HTMLDivElement;
42
+ element: ReactNode;
40
43
  state: ReactRouterState;
41
44
  hydration?: ReactHydrationState;
42
45
  };
43
46
  "react:transition:begin": {
47
+ previous: ReactRouterState;
44
48
  state: ReactRouterState;
49
+ animation?: PageAnimation;
45
50
  };
46
51
  "react:transition:success": {
47
52
  state: ReactRouterState;
@@ -10,7 +10,6 @@ import {
10
10
  import { DateTimeProvider } from "@alepha/datetime";
11
11
  import { $logger } from "@alepha/logger";
12
12
  import { LinkProvider } from "@alepha/server-links";
13
- import { createRoot, hydrateRoot, type Root } from "react-dom/client";
14
13
  import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
15
14
  import type {
16
15
  PreviousLayerData,
@@ -37,7 +36,6 @@ export class ReactBrowserProvider {
37
36
  protected readonly alepha = $inject(Alepha);
38
37
  protected readonly router = $inject(ReactBrowserRouterProvider);
39
38
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
40
- protected root?: Root;
41
39
 
42
40
  public options: ReactBrowserRendererOptions = {
43
41
  scrollRestoration: "top",
@@ -150,6 +148,7 @@ export class ReactBrowserProvider {
150
148
  await this.render({
151
149
  url,
152
150
  previous: options.force ? [] : this.state.layers,
151
+ meta: options.meta,
153
152
  });
154
153
 
155
154
  // when redirecting in browser
@@ -161,9 +160,7 @@ export class ReactBrowserProvider {
161
160
  this.pushState(url, options.replace);
162
161
  }
163
162
 
164
- protected async render(
165
- options: { url?: string; previous?: PreviousLayerData[] } = {},
166
- ): Promise<void> {
163
+ protected async render(options: RouterRenderOptions = {}): Promise<void> {
167
164
  const previous = options.previous ?? this.state.layers;
168
165
  const url = options.url ?? this.url;
169
166
  const start = this.dateTimeProvider.now();
@@ -180,6 +177,7 @@ export class ReactBrowserProvider {
180
177
  const redirect = await this.router.transition(
181
178
  new URL(`http://localhost${url}`),
182
179
  previous,
180
+ options.meta,
183
181
  );
184
182
 
185
183
  if (redirect) {
@@ -215,7 +213,8 @@ export class ReactBrowserProvider {
215
213
  handler: () => {
216
214
  if (
217
215
  this.options.scrollRestoration === "top" &&
218
- typeof window !== "undefined"
216
+ typeof window !== "undefined" &&
217
+ !this.alepha.isTest()
219
218
  ) {
220
219
  this.log.trace("Restoring scroll position to top");
221
220
  window.scrollTo(0, 0);
@@ -241,14 +240,13 @@ export class ReactBrowserProvider {
241
240
  await this.render({ previous });
242
241
 
243
242
  const element = this.router.root(this.state);
244
- if (hydration?.layers) {
245
- this.root = hydrateRoot(this.getRootElement(), element);
246
- this.log.info("Hydrated root element");
247
- } else {
248
- this.root ??= createRoot(this.getRootElement());
249
- this.root.render(element);
250
- this.log.info("Created root element");
251
- }
243
+
244
+ await this.alepha.emit("react:browser:render", {
245
+ element,
246
+ root: this.getRootElement(),
247
+ hydration,
248
+ state: this.state,
249
+ });
252
250
 
253
251
  window.addEventListener("popstate", () => {
254
252
  // when you update silently queryParams or hash, skip rendering
@@ -274,6 +272,7 @@ export interface RouterGoOptions {
274
272
  match?: TransitionOptions;
275
273
  params?: Record<string, string>;
276
274
  query?: Record<string, string>;
275
+ meta?: Record<string, any>;
277
276
 
278
277
  /**
279
278
  * Recreate the whole page, ignoring the current state.
@@ -286,3 +285,9 @@ export type ReactHydrationState = {
286
285
  } & {
287
286
  [key: string]: any;
288
287
  };
288
+
289
+ export interface RouterRenderOptions {
290
+ url?: string;
291
+ previous?: PreviousLayerData[];
292
+ meta?: Record<string, any>;
293
+ }
@@ -0,0 +1,22 @@
1
+ import { $hook } from "@alepha/core";
2
+ import { $logger } from "@alepha/logger";
3
+ import { createRoot, hydrateRoot, type Root } from "react-dom/client";
4
+
5
+ export class ReactBrowserRendererProvider {
6
+ protected readonly log = $logger();
7
+ protected root?: Root;
8
+
9
+ protected readonly onBrowserRender = $hook({
10
+ on: "react:browser:render",
11
+ handler: async ({ hydration, root, element }) => {
12
+ if (hydration?.layers) {
13
+ this.root = hydrateRoot(root, element);
14
+ this.log.info("Hydrated root element");
15
+ } else {
16
+ this.root ??= createRoot(root);
17
+ this.root.render(element);
18
+ this.log.info("Created root element");
19
+ }
20
+ },
21
+ });
22
+ }
@@ -43,6 +43,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
43
43
  public async transition(
44
44
  url: URL,
45
45
  previous: PreviousLayerData[] = [],
46
+ meta = {},
46
47
  ): Promise<string | void> {
47
48
  const { pathname, search } = url;
48
49
 
@@ -52,11 +53,15 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
52
53
  params: {},
53
54
  layers: [],
54
55
  onError: () => null,
56
+ meta,
55
57
  };
56
58
 
57
59
  const state = entry as ReactRouterState;
58
60
 
59
- await this.alepha.emit("react:transition:begin", { state });
61
+ await this.alepha.emit("react:transition:begin", {
62
+ previous: this.alepha.state("react.router.state")!,
63
+ state,
64
+ });
60
65
 
61
66
  try {
62
67
  const { route, params } = this.match(pathname);
@@ -119,11 +124,11 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
119
124
  }
120
125
  }
121
126
 
127
+ this.alepha.state("react.router.state", state);
128
+
122
129
  await this.alepha.emit("react:transition:end", {
123
130
  state,
124
131
  });
125
-
126
- this.alepha.state("react.router.state", state);
127
132
  }
128
133
 
129
134
  public root(state: ReactRouterState): ReactNode {