@alepha/react 0.9.4 → 0.10.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.
@@ -3,14 +3,99 @@ import { AlephaServer, HttpClient } from "@alepha/server";
3
3
  import { AlephaServerLinks, LinkProvider } from "@alepha/server-links";
4
4
  import { DateTimeProvider } from "@alepha/datetime";
5
5
  import { $logger } from "@alepha/logger";
6
- import { createRoot, hydrateRoot } from "react-dom/client";
7
6
  import { RouterProvider } from "@alepha/router";
8
- import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
9
- import { jsx, jsxs } from "react/jsx-runtime";
7
+ import React, { StrictMode, createContext, createElement, memo, use, useContext, useEffect, useMemo, useRef, useState } from "react";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { createRoot, hydrateRoot } from "react-dom/client";
10
10
 
11
11
  //#region src/descriptors/$page.ts
12
12
  /**
13
13
  * Main descriptor for defining a React route in the application.
14
+ *
15
+ * The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
16
+ * It provides a declarative way to define pages with powerful features:
17
+ *
18
+ * **Routing & Navigation**
19
+ * - URL pattern matching with parameters (e.g., `/users/:id`)
20
+ * - Nested routing with parent-child relationships
21
+ * - Type-safe URL parameter and query string validation
22
+ *
23
+ * **Data Loading**
24
+ * - Server-side data fetching with the `resolve` function
25
+ * - Automatic serialization and hydration for SSR
26
+ * - Access to request context, URL params, and parent data
27
+ *
28
+ * **Component Loading**
29
+ * - Direct component rendering or lazy loading for code splitting
30
+ * - Client-only rendering when browser APIs are needed
31
+ * - Automatic fallback handling during hydration
32
+ *
33
+ * **Performance Optimization**
34
+ * - Static generation for pre-rendered pages at build time
35
+ * - Server-side caching with configurable TTL and providers
36
+ * - Code splitting through lazy component loading
37
+ *
38
+ * **Error Handling**
39
+ * - Custom error handlers with support for redirects
40
+ * - Hierarchical error handling (child → parent)
41
+ * - HTTP status code handling (404, 401, etc.)
42
+ *
43
+ * **Page Animations**
44
+ * - CSS-based enter/exit animations
45
+ * - Dynamic animations based on page state
46
+ * - Custom timing and easing functions
47
+ *
48
+ * **Lifecycle Management**
49
+ * - Server response hooks for headers and status codes
50
+ * - Page leave handlers for cleanup (browser only)
51
+ * - Permission-based access control
52
+ *
53
+ * @example Simple page with data fetching
54
+ * ```typescript
55
+ * const userProfile = $page({
56
+ * path: "/users/:id",
57
+ * schema: {
58
+ * params: t.object({ id: t.int() }),
59
+ * query: t.object({ tab: t.optional(t.string()) })
60
+ * },
61
+ * resolve: async ({ params }) => {
62
+ * const user = await userApi.getUser(params.id);
63
+ * return { user };
64
+ * },
65
+ * lazy: () => import("./UserProfile.tsx")
66
+ * });
67
+ * ```
68
+ *
69
+ * @example Nested routing with error handling
70
+ * ```typescript
71
+ * const projectSection = $page({
72
+ * path: "/projects/:id",
73
+ * children: () => [projectBoard, projectSettings],
74
+ * resolve: async ({ params }) => {
75
+ * const project = await projectApi.get(params.id);
76
+ * return { project };
77
+ * },
78
+ * errorHandler: (error) => {
79
+ * if (HttpError.is(error, 404)) {
80
+ * return <ProjectNotFound />;
81
+ * }
82
+ * }
83
+ * });
84
+ * ```
85
+ *
86
+ * @example Static generation with caching
87
+ * ```typescript
88
+ * const blogPost = $page({
89
+ * path: "/blog/:slug",
90
+ * static: {
91
+ * entries: posts.map(p => ({ params: { slug: p.slug } }))
92
+ * },
93
+ * resolve: async ({ params }) => {
94
+ * const post = await loadPost(params.slug);
95
+ * return { post };
96
+ * }
97
+ * });
98
+ * ```
14
99
  */
15
100
  const $page = (options) => {
16
101
  return createDescriptor(PageDescriptor, options);
@@ -30,7 +115,10 @@ var PageDescriptor = class extends Descriptor {
30
115
  * Only valid for server-side rendering, it will throw an error if called on the client-side.
31
116
  */
32
117
  async render(options) {
33
- throw new Error("render method is not implemented in this environment");
118
+ throw new AlephaError("render() method is not implemented in this environment");
119
+ }
120
+ async fetch(options) {
121
+ throw new AlephaError("fetch() method is not implemented in this environment");
34
122
  }
35
123
  match(url) {
36
124
  return false;
@@ -84,7 +172,6 @@ const ClientOnly = (props) => {
84
172
  if (props.disabled) return props.children;
85
173
  return mounted ? props.children : props.fallback;
86
174
  };
87
- var ClientOnly_default = ClientOnly;
88
175
 
89
176
  //#endregion
90
177
  //#region src/components/ErrorViewer.tsx
@@ -192,7 +279,6 @@ const ErrorViewer = ({ error, alepha }) => {
192
279
  })] })]
193
280
  });
194
281
  };
195
- var ErrorViewer_default = ErrorViewer;
196
282
  const ErrorViewerProduction = () => {
197
283
  const styles = {
198
284
  container: {
@@ -268,7 +354,7 @@ const AlephaContext = createContext(void 0);
268
354
  *
269
355
  * - alepha.state() for state management
270
356
  * - alepha.inject() for dependency injection
271
- * - alepha.emit() for event handling
357
+ * - alepha.events.emit() for event handling
272
358
  * etc...
273
359
  */
274
360
  const useAlepha = () => {
@@ -286,19 +372,55 @@ const useRouterEvents = (opts = {}, deps = []) => {
286
372
  const alepha = useAlepha();
287
373
  useEffect(() => {
288
374
  if (!alepha.isBrowser()) return;
375
+ const cb = (callback) => {
376
+ if (typeof callback === "function") return { callback };
377
+ return callback;
378
+ };
289
379
  const subs = [];
290
380
  const onBegin = opts.onBegin;
291
381
  const onEnd = opts.onEnd;
292
382
  const onError = opts.onError;
293
- if (onBegin) subs.push(alepha.on("react:transition:begin", { callback: onBegin }));
294
- if (onEnd) subs.push(alepha.on("react:transition:end", { callback: onEnd }));
295
- if (onError) subs.push(alepha.on("react:transition:error", { callback: onError }));
383
+ const onSuccess = opts.onSuccess;
384
+ if (onBegin) subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
385
+ if (onEnd) subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
386
+ if (onError) subs.push(alepha.events.on("react:transition:error", cb(onError)));
387
+ if (onSuccess) subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
296
388
  return () => {
297
389
  for (const sub of subs) sub();
298
390
  };
299
391
  }, deps);
300
392
  };
301
393
 
394
+ //#endregion
395
+ //#region src/hooks/useStore.ts
396
+ /**
397
+ * Hook to access and mutate the Alepha state.
398
+ */
399
+ const useStore = (key, defaultValue) => {
400
+ const alepha = useAlepha();
401
+ useMemo(() => {
402
+ if (defaultValue != null && alepha.state.get(key) == null) alepha.state.set(key, defaultValue);
403
+ }, [defaultValue]);
404
+ const [state, setState] = useState(alepha.state.get(key));
405
+ useEffect(() => {
406
+ if (!alepha.isBrowser()) return;
407
+ return alepha.events.on("state:mutate", (ev) => {
408
+ if (ev.key === key) setState(ev.value);
409
+ });
410
+ }, []);
411
+ return [state, (value) => {
412
+ alepha.state.set(key, value);
413
+ }];
414
+ };
415
+
416
+ //#endregion
417
+ //#region src/hooks/useRouterState.ts
418
+ const useRouterState = () => {
419
+ const [state] = useStore("react.router.state");
420
+ if (!state) throw new AlephaError("Missing react router state");
421
+ return state;
422
+ };
423
+
302
424
  //#endregion
303
425
  //#region src/components/ErrorBoundary.tsx
304
426
  /**
@@ -328,7 +450,6 @@ var ErrorBoundary = class extends React.Component {
328
450
  return this.props.children;
329
451
  }
330
452
  };
331
- var ErrorBoundary_default = ErrorBoundary;
332
453
 
333
454
  //#endregion
334
455
  //#region src/components/NestedView.tsx
@@ -339,7 +460,7 @@ var ErrorBoundary_default = ErrorBoundary;
339
460
  *
340
461
  * @example
341
462
  * ```tsx
342
- * import { NestedView } from "@alepha/react";
463
+ * import { NestedView } from "alepha/react";
343
464
  *
344
465
  * class App {
345
466
  * parent = $page({
@@ -354,17 +475,69 @@ var ErrorBoundary_default = ErrorBoundary;
354
475
  * ```
355
476
  */
356
477
  const NestedView = (props) => {
357
- const layer = useContext(RouterLayerContext);
358
- const index = layer?.index ?? 0;
359
- const alepha = useAlepha();
360
- const state = alepha.state("react.router.state");
361
- if (!state) throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
478
+ const index = use(RouterLayerContext)?.index ?? 0;
479
+ const state = useRouterState();
362
480
  const [view, setView] = useState(state.layers[index]?.element);
363
- useRouterEvents({ onEnd: ({ state: state$1 }) => {
364
- if (!state$1.layers[index]?.cache) setView(state$1.layers[index]?.element);
365
- } }, []);
366
- const element = view ?? props.children ?? null;
367
- return /* @__PURE__ */ jsx(ErrorBoundary_default, {
481
+ const [animation, setAnimation] = useState("");
482
+ const animationExitDuration = useRef(0);
483
+ const animationExitNow = useRef(0);
484
+ useRouterEvents({
485
+ onBegin: async ({ previous, state: state$1 }) => {
486
+ const layer = previous.layers[index];
487
+ if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
488
+ const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
489
+ if (animationExit) {
490
+ const duration = animationExit.duration || 200;
491
+ animationExitNow.current = Date.now();
492
+ animationExitDuration.current = duration;
493
+ setAnimation(animationExit.animation);
494
+ } else {
495
+ animationExitNow.current = 0;
496
+ animationExitDuration.current = 0;
497
+ setAnimation("");
498
+ }
499
+ },
500
+ onEnd: async ({ state: state$1 }) => {
501
+ const layer = state$1.layers[index];
502
+ if (animationExitNow.current) {
503
+ const duration = animationExitDuration.current;
504
+ const diff = Date.now() - animationExitNow.current;
505
+ if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
506
+ }
507
+ if (!layer?.cache) {
508
+ setView(layer?.element);
509
+ const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
510
+ if (animationEnter) setAnimation(animationEnter.animation);
511
+ else setAnimation("");
512
+ }
513
+ }
514
+ }, []);
515
+ let element = view ?? props.children ?? null;
516
+ if (animation) element = /* @__PURE__ */ jsx("div", {
517
+ style: {
518
+ display: "flex",
519
+ flex: 1,
520
+ height: "100%",
521
+ width: "100%",
522
+ position: "relative",
523
+ overflow: "hidden"
524
+ },
525
+ children: /* @__PURE__ */ jsx("div", {
526
+ style: {
527
+ height: "100%",
528
+ width: "100%",
529
+ display: "flex",
530
+ animation
531
+ },
532
+ children: element
533
+ })
534
+ });
535
+ if (props.errorBoundary === false) return /* @__PURE__ */ jsx(Fragment, { children: element });
536
+ if (props.errorBoundary) return /* @__PURE__ */ jsx(ErrorBoundary, {
537
+ fallback: props.errorBoundary,
538
+ children: element
539
+ });
540
+ return /* @__PURE__ */ jsx(ErrorBoundary, {
368
541
  fallback: (error) => {
369
542
  const result = state.onError(error, state);
370
543
  if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
@@ -373,7 +546,37 @@ const NestedView = (props) => {
373
546
  children: element
374
547
  });
375
548
  };
376
- var NestedView_default = NestedView;
549
+ var NestedView_default = memo(NestedView);
550
+ function parseAnimation(animationLike, state, type = "enter") {
551
+ if (!animationLike) return void 0;
552
+ const DEFAULT_DURATION = 300;
553
+ const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
554
+ if (typeof animation === "string") {
555
+ if (type === "exit") return;
556
+ return {
557
+ duration: DEFAULT_DURATION,
558
+ animation: `${DEFAULT_DURATION}ms ${animation}`
559
+ };
560
+ }
561
+ if (typeof animation === "object") {
562
+ const anim = animation[type];
563
+ const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
564
+ const name = typeof anim === "object" ? anim.name : anim;
565
+ if (type === "exit") {
566
+ const timing$1 = typeof anim === "object" ? anim.timing ?? "" : "";
567
+ return {
568
+ duration,
569
+ animation: `${duration}ms ${timing$1} ${name}`
570
+ };
571
+ }
572
+ const timing = typeof anim === "object" ? anim.timing ?? "" : "";
573
+ return {
574
+ duration,
575
+ animation: `${duration}ms ${timing} ${name}`
576
+ };
577
+ }
578
+ return void 0;
579
+ }
377
580
 
378
581
  //#endregion
379
582
  //#region src/providers/ReactPageProvider.ts
@@ -414,6 +617,14 @@ var ReactPageProvider = class {
414
617
  if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
415
618
  return root;
416
619
  }
620
+ convertStringObjectToObject = (schema, value) => {
621
+ if (t.schema.isObject(schema) && typeof value === "object") {
622
+ for (const key in schema.properties) if (t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
623
+ value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
624
+ } catch (e) {}
625
+ }
626
+ return value;
627
+ };
417
628
  /**
418
629
  * Create a new RouterState based on a given route and request.
419
630
  * This method resolves the layers for the route, applying any query and params schemas defined in the route.
@@ -433,6 +644,7 @@ var ReactPageProvider = class {
433
644
  const route$1 = it.route;
434
645
  const config = {};
435
646
  try {
647
+ this.convertStringObjectToObject(route$1.schema?.query, state.query);
436
648
  config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
437
649
  } catch (e) {
438
650
  it.error = e;
@@ -559,6 +771,7 @@ var ReactPageProvider = class {
559
771
  }
560
772
  }
561
773
  async createElement(page, props) {
774
+ if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
562
775
  if (page.lazy) {
563
776
  const component = await page.lazy();
564
777
  return createElement(component.default, props);
@@ -567,7 +780,7 @@ var ReactPageProvider = class {
567
780
  return void 0;
568
781
  }
569
782
  renderError(error) {
570
- return createElement(ErrorViewer_default, {
783
+ return createElement(ErrorViewer, {
571
784
  error,
572
785
  alepha: this.alepha
573
786
  });
@@ -593,7 +806,7 @@ var ReactPageProvider = class {
593
806
  }
594
807
  renderView(index, path, view, page) {
595
808
  view ??= this.renderEmptyView();
596
- const element = page.client ? createElement(ClientOnly_default, typeof page.client === "object" ? page.client : {}, view) : view;
809
+ const element = page.client ? createElement(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
597
810
  return createElement(RouterLayerContext.Provider, { value: {
598
811
  index,
599
812
  path
@@ -692,17 +905,21 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
692
905
  });
693
906
  }
694
907
  });
695
- async transition(url, previous = []) {
908
+ async transition(url, previous = [], meta = {}) {
696
909
  const { pathname, search } = url;
697
910
  const entry = {
698
911
  url,
699
912
  query: {},
700
913
  params: {},
701
914
  layers: [],
702
- onError: () => null
915
+ onError: () => null,
916
+ meta
703
917
  };
704
918
  const state = entry;
705
- await this.alepha.emit("react:transition:begin", { state });
919
+ await this.alepha.events.emit("react:transition:begin", {
920
+ previous: this.alepha.state.get("react.router.state"),
921
+ state
922
+ });
706
923
  try {
707
924
  const { route, params } = this.match(pathname);
708
925
  const query = {};
@@ -719,7 +936,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
719
936
  index: 0,
720
937
  path: "/"
721
938
  });
722
- await this.alepha.emit("react:transition:success", { state });
939
+ await this.alepha.events.emit("react:transition:success", { state });
723
940
  } catch (e) {
724
941
  this.log.error("Transition has failed", e);
725
942
  state.layers = [{
@@ -728,7 +945,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
728
945
  index: 0,
729
946
  path: "/"
730
947
  }];
731
- await this.alepha.emit("react:transition:error", {
948
+ await this.alepha.events.emit("react:transition:error", {
732
949
  error: e,
733
950
  state
734
951
  });
@@ -737,8 +954,8 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
737
954
  const layer = previous[i];
738
955
  if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
739
956
  }
740
- await this.alepha.emit("react:transition:end", { state });
741
- this.alepha.state("react.router.state", state);
957
+ this.alepha.state.set("react.router.state", state);
958
+ await this.alepha.events.emit("react:transition:end", { state });
742
959
  }
743
960
  root(state) {
744
961
  return this.pageApi.root(state);
@@ -755,7 +972,6 @@ var ReactBrowserProvider = class {
755
972
  alepha = $inject(Alepha);
756
973
  router = $inject(ReactBrowserRouterProvider);
757
974
  dateTimeProvider = $inject(DateTimeProvider);
758
- root;
759
975
  options = { scrollRestoration: "top" };
760
976
  getRootElement() {
761
977
  const root = this.document.getElementById(this.env.REACT_ROOT_ID);
@@ -767,7 +983,7 @@ var ReactBrowserProvider = class {
767
983
  }
768
984
  transitioning;
769
985
  get state() {
770
- return this.alepha.state("react.router.state");
986
+ return this.alepha.state.get("react.router.state");
771
987
  }
772
988
  /**
773
989
  * Accessor for Document DOM API.
@@ -831,7 +1047,8 @@ var ReactBrowserProvider = class {
831
1047
  });
832
1048
  await this.render({
833
1049
  url,
834
- previous: options.force ? [] : this.state.layers
1050
+ previous: options.force ? [] : this.state.layers,
1051
+ meta: options.meta
835
1052
  });
836
1053
  if (this.state.url.pathname + this.state.url.search !== url) {
837
1054
  this.pushState(this.state.url.pathname + this.state.url.search);
@@ -848,7 +1065,7 @@ var ReactBrowserProvider = class {
848
1065
  from: this.state?.url.pathname
849
1066
  };
850
1067
  this.log.debug("Transitioning...", { to: url });
851
- const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
1068
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
852
1069
  if (redirect) {
853
1070
  this.log.info("Redirecting to", { redirect });
854
1071
  return await this.render({ url: redirect });
@@ -870,7 +1087,7 @@ var ReactBrowserProvider = class {
870
1087
  onTransitionEnd = $hook({
871
1088
  on: "react:transition:end",
872
1089
  handler: () => {
873
- if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
1090
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
874
1091
  this.log.trace("Restoring scroll position to top");
875
1092
  window.scrollTo(0, 0);
876
1093
  }
@@ -882,23 +1099,41 @@ var ReactBrowserProvider = class {
882
1099
  const hydration = this.getHydrationState();
883
1100
  const previous = hydration?.layers ?? [];
884
1101
  if (hydration) {
885
- for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
1102
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state.set(key, value);
886
1103
  }
887
1104
  await this.render({ previous });
888
1105
  const element = this.router.root(this.state);
1106
+ await this.alepha.events.emit("react:browser:render", {
1107
+ element,
1108
+ root: this.getRootElement(),
1109
+ hydration,
1110
+ state: this.state
1111
+ });
1112
+ window.addEventListener("popstate", () => {
1113
+ if (this.base + this.state.url.pathname === this.location.pathname) return;
1114
+ this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
1115
+ this.render();
1116
+ });
1117
+ }
1118
+ });
1119
+ };
1120
+
1121
+ //#endregion
1122
+ //#region src/providers/ReactBrowserRendererProvider.ts
1123
+ var ReactBrowserRendererProvider = class {
1124
+ log = $logger();
1125
+ root;
1126
+ onBrowserRender = $hook({
1127
+ on: "react:browser:render",
1128
+ handler: async ({ hydration, root, element }) => {
889
1129
  if (hydration?.layers) {
890
- this.root = hydrateRoot(this.getRootElement(), element);
1130
+ this.root = hydrateRoot(root, element);
891
1131
  this.log.info("Hydrated root element");
892
1132
  } else {
893
- this.root ??= createRoot(this.getRootElement());
1133
+ this.root ??= createRoot(root);
894
1134
  this.root.render(element);
895
1135
  this.log.info("Created root element");
896
1136
  }
897
- window.addEventListener("popstate", () => {
898
- if (this.base + this.state.url.pathname === this.location.pathname) return;
899
- this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
900
- this.render();
901
- });
902
1137
  }
903
1138
  });
904
1139
  };
@@ -909,7 +1144,7 @@ var ReactRouter = class {
909
1144
  alepha = $inject(Alepha);
910
1145
  pageApi = $inject(ReactPageProvider);
911
1146
  get state() {
912
- return this.alepha.state("react.router.state");
1147
+ return this.alepha.state.get("react.router.state");
913
1148
  }
914
1149
  get pages() {
915
1150
  return this.pageApi.getPages();
@@ -1032,44 +1267,12 @@ const useRouter = () => {
1032
1267
  //#region src/components/Link.tsx
1033
1268
  const Link = (props) => {
1034
1269
  const router = useRouter();
1035
- const { to,...anchorProps } = props;
1036
1270
  return /* @__PURE__ */ jsx("a", {
1037
- ...router.anchor(to),
1038
- ...anchorProps,
1271
+ ...props,
1272
+ ...router.anchor(props.href),
1039
1273
  children: props.children
1040
1274
  });
1041
1275
  };
1042
- var Link_default = Link;
1043
-
1044
- //#endregion
1045
- //#region src/hooks/useStore.ts
1046
- /**
1047
- * Hook to access and mutate the Alepha state.
1048
- */
1049
- const useStore = (key, defaultValue) => {
1050
- const alepha = useAlepha();
1051
- useMemo(() => {
1052
- if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
1053
- }, [defaultValue]);
1054
- const [state, setState] = useState(alepha.state(key));
1055
- useEffect(() => {
1056
- if (!alepha.isBrowser()) return;
1057
- return alepha.on("state:mutate", (ev) => {
1058
- if (ev.key === key) setState(ev.value);
1059
- });
1060
- }, []);
1061
- return [state, (value) => {
1062
- alepha.state(key, value);
1063
- }];
1064
- };
1065
-
1066
- //#endregion
1067
- //#region src/hooks/useRouterState.ts
1068
- const useRouterState = () => {
1069
- const [state] = useStore("react.router.state");
1070
- if (!state) throw new AlephaError("Missing react router state");
1071
- return state;
1072
- };
1073
1276
 
1074
1277
  //#endregion
1075
1278
  //#region src/hooks/useActive.ts
@@ -1127,7 +1330,7 @@ const useQueryParams = (schema, options = {}) => {
1127
1330
  const key = options.key ?? "q";
1128
1331
  const router = useRouter();
1129
1332
  const querystring = router.query[key];
1130
- const [queryParams, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
1333
+ const [queryParams = {}, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
1131
1334
  useEffect(() => {
1132
1335
  setQueryParams(decode(alepha, schema, querystring));
1133
1336
  }, [querystring]);
@@ -1147,8 +1350,8 @@ const encode = (alepha, schema, data) => {
1147
1350
  const decode = (alepha, schema, data) => {
1148
1351
  try {
1149
1352
  return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
1150
- } catch (_error) {
1151
- return {};
1353
+ } catch {
1354
+ return;
1152
1355
  }
1153
1356
  };
1154
1357
 
@@ -1196,11 +1399,12 @@ const AlephaReact = $module({
1196
1399
  ReactPageProvider,
1197
1400
  ReactBrowserRouterProvider,
1198
1401
  ReactBrowserProvider,
1199
- ReactRouter
1402
+ ReactRouter,
1403
+ ReactBrowserRendererProvider
1200
1404
  ],
1201
- register: (alepha) => alepha.with(AlephaServer).with(AlephaServerLinks).with(ReactPageProvider).with(ReactBrowserProvider).with(ReactBrowserRouterProvider).with(ReactRouter)
1405
+ register: (alepha) => alepha.with(AlephaServer).with(AlephaServerLinks).with(ReactPageProvider).with(ReactBrowserProvider).with(ReactBrowserRouterProvider).with(ReactBrowserRendererProvider).with(ReactRouter)
1202
1406
  });
1203
1407
 
1204
1408
  //#endregion
1205
- export { $page, AlephaContext, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, ErrorViewer_default as ErrorViewer, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, ReactBrowserProvider, ReactBrowserRouterProvider, ReactPageProvider, ReactRouter, Redirection, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
1409
+ export { $page, AlephaContext, AlephaReact, ClientOnly, ErrorBoundary, ErrorViewer, Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, ReactBrowserProvider, ReactBrowserRouterProvider, ReactPageProvider, ReactRouter, Redirection, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
1206
1410
  //# sourceMappingURL=index.browser.js.map