@alepha/react 0.9.0 → 0.9.2

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.
@@ -1,6 +1,6 @@
1
1
  import type { Alepha, Static, TObject } from "@alepha/core";
2
- import { useContext, useEffect, useState } from "react";
3
- import { RouterContext } from "../contexts/RouterContext.ts";
2
+ import { useEffect, useState } from "react";
3
+ import { useAlepha } from "./useAlepha.ts";
4
4
  import { useRouter } from "./useRouter.ts";
5
5
 
6
6
  export interface UseQueryParamsHookOptions {
@@ -13,21 +13,18 @@ export const useQueryParams = <T extends TObject>(
13
13
  schema: T,
14
14
  options: UseQueryParamsHookOptions = {},
15
15
  ): [Static<T>, (data: Static<T>) => void] => {
16
- const ctx = useContext(RouterContext);
17
- if (!ctx) {
18
- throw new Error("useQueryParams must be used within a RouterProvider");
19
- }
16
+ const alepha = useAlepha();
20
17
 
21
18
  const key = options.key ?? "q";
22
19
  const router = useRouter();
23
20
  const querystring = router.query[key];
24
21
 
25
22
  const [queryParams, setQueryParams] = useState(
26
- decode(ctx.alepha, schema, router.query[key]),
23
+ decode(alepha, schema, router.query[key]),
27
24
  );
28
25
 
29
26
  useEffect(() => {
30
- setQueryParams(decode(ctx.alepha, schema, querystring));
27
+ setQueryParams(decode(alepha, schema, querystring));
31
28
  }, [querystring]);
32
29
 
33
30
  return [
@@ -35,7 +32,7 @@ export const useQueryParams = <T extends TObject>(
35
32
  (queryParams: Static<T>) => {
36
33
  setQueryParams(queryParams);
37
34
  router.setQueryParams((data) => {
38
- return { ...data, [key]: encode(ctx.alepha, schema, queryParams) };
35
+ return { ...data, [key]: encode(alepha, schema, queryParams) };
39
36
  });
40
37
  },
41
38
  ];
@@ -4,8 +4,10 @@ import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
4
4
  import { PageDescriptorProvider } from "../providers/PageDescriptorProvider.ts";
5
5
  import { ReactBrowserProvider } from "../providers/ReactBrowserProvider.ts";
6
6
  import { RouterHookApi } from "./RouterHookApi.ts";
7
+ import { useAlepha } from "./useAlepha.ts";
7
8
 
8
9
  export const useRouter = (): RouterHookApi => {
10
+ const alepha = useAlepha();
9
11
  const ctx = useContext(RouterContext);
10
12
  const layer = useContext(RouterLayerContext);
11
13
  if (!ctx || !layer) {
@@ -13,7 +15,7 @@ export const useRouter = (): RouterHookApi => {
13
15
  }
14
16
 
15
17
  const pages = useMemo(() => {
16
- return ctx.alepha.inject(PageDescriptorProvider).getPages();
18
+ return alepha.inject(PageDescriptorProvider).getPages();
17
19
  }, []);
18
20
 
19
21
  return useMemo(
@@ -23,9 +25,7 @@ export const useRouter = (): RouterHookApi => {
23
25
  ctx.context,
24
26
  ctx.state,
25
27
  layer,
26
- ctx.alepha.isBrowser()
27
- ? ctx.alepha.inject(ReactBrowserProvider)
28
- : undefined,
28
+ alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : undefined,
29
29
  ),
30
30
  [layer],
31
31
  );
@@ -1,6 +1,6 @@
1
- import { useContext, useEffect } from "react";
2
- import { RouterContext } from "../contexts/RouterContext.ts";
1
+ import { useEffect } from "react";
3
2
  import type { RouterState } from "../providers/PageDescriptorProvider.ts";
3
+ import { useAlepha } from "./useAlepha.ts";
4
4
 
5
5
  export const useRouterEvents = (
6
6
  opts: {
@@ -10,13 +10,10 @@ export const useRouterEvents = (
10
10
  } = {},
11
11
  deps: any[] = [],
12
12
  ) => {
13
- const ctx = useContext(RouterContext);
14
- if (!ctx) {
15
- throw new Error("useRouter must be used within a RouterProvider");
16
- }
13
+ const alepha = useAlepha();
17
14
 
18
15
  useEffect(() => {
19
- if (!ctx.alepha.isBrowser()) {
16
+ if (!alepha.isBrowser()) {
20
17
  return;
21
18
  }
22
19
 
@@ -27,7 +24,7 @@ export const useRouterEvents = (
27
24
 
28
25
  if (onBegin) {
29
26
  subs.push(
30
- ctx.alepha.on("react:transition:begin", {
27
+ alepha.on("react:transition:begin", {
31
28
  callback: onBegin,
32
29
  }),
33
30
  );
@@ -35,7 +32,7 @@ export const useRouterEvents = (
35
32
 
36
33
  if (onEnd) {
37
34
  subs.push(
38
- ctx.alepha.on("react:transition:end", {
35
+ alepha.on("react:transition:end", {
39
36
  callback: onEnd,
40
37
  }),
41
38
  );
@@ -43,7 +40,7 @@ export const useRouterEvents = (
43
40
 
44
41
  if (onError) {
45
42
  subs.push(
46
- ctx.alepha.on("react:transition:error", {
43
+ alepha.on("react:transition:error", {
47
44
  callback: onError,
48
45
  }),
49
46
  );
@@ -5,13 +5,15 @@ import type { RouterState } from "../providers/PageDescriptorProvider.ts";
5
5
  import { useRouterEvents } from "./useRouterEvents.ts";
6
6
 
7
7
  export const useRouterState = (): RouterState => {
8
- const ctx = useContext(RouterContext);
8
+ const router = useContext(RouterContext);
9
9
  const layer = useContext(RouterLayerContext);
10
- if (!ctx || !layer) {
11
- throw new Error("useRouter must be used within a RouterProvider");
10
+ if (!router || !layer) {
11
+ throw new Error(
12
+ "useRouterState must be used within a RouterContext.Provider",
13
+ );
12
14
  }
13
15
 
14
- const [state, setState] = useState(ctx.state);
16
+ const [state, setState] = useState(router.state);
15
17
 
16
18
  useRouterEvents({
17
19
  onEnd: ({ state }) => setState({ ...state }),
@@ -0,0 +1,93 @@
1
+ import type { Alepha } from "@alepha/core";
2
+ import {
3
+ type FetchOptions,
4
+ HttpClient,
5
+ type RequestConfigSchema,
6
+ } from "@alepha/server";
7
+ import {
8
+ type HttpClientLink,
9
+ LinkProvider,
10
+ type VirtualAction,
11
+ } from "@alepha/server-links";
12
+ import { useEffect, useState } from "react";
13
+ import { useAlepha } from "./useAlepha.ts";
14
+ import { useInject } from "./useInject.ts";
15
+
16
+ export const useSchema = <TConfig extends RequestConfigSchema>(
17
+ action: VirtualAction<TConfig>,
18
+ ): UseSchemaReturn<TConfig> => {
19
+ const name = action.name;
20
+ const alepha = useAlepha();
21
+ const httpClient = useInject(HttpClient);
22
+ const linkProvider = useInject(LinkProvider);
23
+ const [schema, setSchema] = useState<UseSchemaReturn<TConfig>>(
24
+ ssrSchemaLoading(alepha, name) as UseSchemaReturn<TConfig>,
25
+ );
26
+
27
+ useEffect(() => {
28
+ if (!schema.loading) {
29
+ return;
30
+ }
31
+
32
+ const opts: FetchOptions = {
33
+ cache: true,
34
+ };
35
+
36
+ httpClient
37
+ .fetch(`${linkProvider.URL_LINKS}/${name}/schema`, {}, opts)
38
+ .then((it) => setSchema(it.data as UseSchemaReturn<TConfig>));
39
+ }, [name]);
40
+
41
+ return schema;
42
+ };
43
+
44
+ export type UseSchemaReturn<TConfig extends RequestConfigSchema> = TConfig & {
45
+ loading: boolean;
46
+ };
47
+
48
+ // ---------------------------------------------------------------------------------------------------------------------
49
+
50
+ /**
51
+ * Get an action schema during server-side rendering (SSR) or client-side rendering (CSR).
52
+ */
53
+ export const ssrSchemaLoading = (alepha: Alepha, name: string) => {
54
+ // server-side rendering (SSR) context
55
+ if (!alepha.isBrowser()) {
56
+ // get user links
57
+ const links =
58
+ alepha.context.get<{ links: HttpClientLink[] }>("links")?.links ?? [];
59
+
60
+ // check if user can access the link
61
+ const can = links.find((it) => it.name === name);
62
+
63
+ // yes!
64
+ if (can) {
65
+ // user-links have no schema, so we need to get it from the provider
66
+ const schema = alepha
67
+ .inject(LinkProvider)
68
+ .links?.find((it) => it.name === name)?.schema;
69
+
70
+ // oh, we have a schema!
71
+ if (schema) {
72
+ // attach to user link, it will be used in the client during hydration :)
73
+ can.schema = schema;
74
+ return schema;
75
+ }
76
+ }
77
+ return { loading: true };
78
+ }
79
+
80
+ // browser side rendering (CSR) context
81
+ // check if we have the schema already loaded
82
+ const schema = alepha
83
+ .inject(LinkProvider)
84
+ .links?.find((it) => it.name === name)?.schema;
85
+
86
+ // yes!
87
+ if (schema) {
88
+ return schema;
89
+ }
90
+
91
+ // no, we need to load it
92
+ return { loading: true };
93
+ };
@@ -0,0 +1,39 @@
1
+ import type { State } from "@alepha/core";
2
+ import { useEffect, useState } from "react";
3
+ import { useAlepha } from "./useAlepha.ts";
4
+
5
+ /**
6
+ * Hook to access and mutate the Alepha state.
7
+ */
8
+ export const useStore = <Key extends keyof State>(
9
+ key: Key,
10
+ ): [State[Key], (value: State[Key]) => void] => {
11
+ const alepha = useAlepha();
12
+ const [state, setState] = useState(alepha.state(key));
13
+
14
+ useEffect(() => {
15
+ if (!alepha.isBrowser()) {
16
+ return;
17
+ }
18
+
19
+ return alepha.on("state:mutate", (ev) => {
20
+ if (ev.key === key) {
21
+ setState(ev.value);
22
+ }
23
+ });
24
+ }, []);
25
+
26
+ if (!alepha.isBrowser()) {
27
+ const value = alepha.context.get(key) as State[Key];
28
+ if (value !== null) {
29
+ return [value, (_: State[Key]) => {}] as const;
30
+ }
31
+ }
32
+
33
+ return [
34
+ state,
35
+ (value: State[Key]) => {
36
+ alepha.state(key, value);
37
+ },
38
+ ] as const;
39
+ };
@@ -4,6 +4,7 @@ export * from "./components/ErrorViewer.tsx";
4
4
  export { default as Link } from "./components/Link.tsx";
5
5
  export { default as NestedView } from "./components/NestedView.tsx";
6
6
  export { default as NotFound } from "./components/NotFound.tsx";
7
+ export * from "./contexts/AlephaContext.ts";
7
8
  export * from "./contexts/RouterContext.ts";
8
9
  export * from "./contexts/RouterLayerContext.ts";
9
10
  export * from "./descriptors/$page.ts";
@@ -17,3 +18,5 @@ export * from "./hooks/useQueryParams.ts";
17
18
  export * from "./hooks/useRouter.ts";
18
19
  export * from "./hooks/useRouterEvents.ts";
19
20
  export * from "./hooks/useRouterState.ts";
21
+ export * from "./hooks/useSchema.ts";
22
+ export * from "./hooks/useStore.ts";
@@ -13,6 +13,7 @@ import ClientOnly from "../components/ClientOnly.tsx";
13
13
  import ErrorViewer from "../components/ErrorViewer.tsx";
14
14
  import NestedView from "../components/NestedView.tsx";
15
15
  import NotFoundPage from "../components/NotFound.tsx";
16
+ import { AlephaContext } from "../contexts/AlephaContext.ts";
16
17
  import { RouterContext } from "../contexts/RouterContext.ts";
17
18
  import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
18
19
  import {
@@ -76,15 +77,18 @@ export class PageDescriptorProvider {
76
77
 
77
78
  public root(state: RouterState, context: PageReactContext): ReactNode {
78
79
  const root = createElement(
79
- RouterContext.Provider,
80
- {
81
- value: {
82
- alepha: this.alepha,
83
- state,
84
- context,
80
+ AlephaContext.Provider,
81
+ { value: this.alepha },
82
+ createElement(
83
+ RouterContext.Provider,
84
+ {
85
+ value: {
86
+ state,
87
+ context,
88
+ },
85
89
  },
86
- },
87
- createElement(NestedView, {}, state.layers[0]?.element),
90
+ createElement(NestedView, {}, state.layers[0]?.element),
91
+ ),
88
92
  );
89
93
 
90
94
  if (this.env.REACT_STRICT_MODE) {
@@ -300,7 +304,7 @@ export class PageDescriptorProvider {
300
304
  }
301
305
 
302
306
  public renderError(error: Error): ReactNode {
303
- return createElement(ErrorViewer, { error });
307
+ return createElement(ErrorViewer, { error, alepha: this.alepha });
304
308
  }
305
309
 
306
310
  public renderEmptyView(): ReactNode {
@@ -174,7 +174,7 @@ export class ReactBrowserProvider {
174
174
  window.addEventListener("popstate", () => {
175
175
  // when you update silently queryparams or hash, skip rendering
176
176
  // if you want to force a rendering, use #go()
177
- if (this.state.pathname === location.pathname) {
177
+ if (this.state.pathname === this.url) {
178
178
  return;
179
179
  }
180
180
 
@@ -36,12 +36,16 @@ const envSchema = t.object({
36
36
  REACT_SERVER_PREFIX: t.string({ default: "" }),
37
37
  REACT_SSR_ENABLED: t.optional(t.boolean()),
38
38
  REACT_ROOT_ID: t.string({ default: "root" }),
39
+ REACT_SERVER_TEMPLATE: t.optional(
40
+ t.string({
41
+ size: "rich",
42
+ }),
43
+ ),
39
44
  });
40
45
 
41
46
  declare module "@alepha/core" {
42
47
  interface Env extends Partial<Static<typeof envSchema>> {}
43
48
  interface State {
44
- "react.server.template"?: string;
45
49
  "react.server.ssr"?: boolean;
46
50
  }
47
51
  }
@@ -125,7 +129,7 @@ export class ReactServerProvider {
125
129
 
126
130
  public get template() {
127
131
  return (
128
- this.alepha.state("react.server.template") ??
132
+ this.alepha.env.REACT_SERVER_TEMPLATE ??
129
133
  "<!DOCTYPE html><html lang='en'><head></head><body></body></html>"
130
134
  );
131
135
  }