@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.
- package/README.md +7 -0
- package/dist/index.browser.js +104 -41
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +109 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -31
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +56 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +107 -43
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/ErrorViewer.tsx +4 -3
- package/src/contexts/AlephaContext.ts +4 -0
- package/src/contexts/RouterContext.ts +0 -2
- package/src/hooks/useAlepha.ts +5 -5
- package/src/hooks/useInject.ts +5 -8
- package/src/hooks/useQueryParams.ts +6 -9
- package/src/hooks/useRouter.ts +4 -4
- package/src/hooks/useRouterEvents.ts +7 -10
- package/src/hooks/useRouterState.ts +6 -4
- package/src/hooks/useSchema.ts +93 -0
- package/src/hooks/useStore.ts +39 -0
- package/src/index.shared.ts +3 -0
- package/src/providers/PageDescriptorProvider.ts +13 -9
- package/src/providers/ReactBrowserProvider.ts +1 -1
- package/src/providers/ReactServerProvider.ts +6 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Alepha, Static, TObject } from "@alepha/core";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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(
|
|
23
|
+
decode(alepha, schema, router.query[key]),
|
|
27
24
|
);
|
|
28
25
|
|
|
29
26
|
useEffect(() => {
|
|
30
|
-
setQueryParams(decode(
|
|
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(
|
|
35
|
+
return { ...data, [key]: encode(alepha, schema, queryParams) };
|
|
39
36
|
});
|
|
40
37
|
},
|
|
41
38
|
];
|
package/src/hooks/useRouter.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8
|
+
const router = useContext(RouterContext);
|
|
9
9
|
const layer = useContext(RouterLayerContext);
|
|
10
|
-
if (!
|
|
11
|
-
throw new Error(
|
|
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(
|
|
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
|
+
};
|
package/src/index.shared.ts
CHANGED
|
@@ -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
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
AlephaContext.Provider,
|
|
81
|
+
{ value: this.alepha },
|
|
82
|
+
createElement(
|
|
83
|
+
RouterContext.Provider,
|
|
84
|
+
{
|
|
85
|
+
value: {
|
|
86
|
+
state,
|
|
87
|
+
context,
|
|
88
|
+
},
|
|
85
89
|
},
|
|
86
|
-
|
|
87
|
-
|
|
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 ===
|
|
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.
|
|
132
|
+
this.alepha.env.REACT_SERVER_TEMPLATE ??
|
|
129
133
|
"<!DOCTYPE html><html lang='en'><head></head><body></body></html>"
|
|
130
134
|
);
|
|
131
135
|
}
|