react-stateshape 0.2.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.
- package/LICENSE +21 -0
- package/README.md +475 -0
- package/dist/index.cjs +320 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.mjs +300 -0
- package/index.ts +24 -0
- package/package.json +49 -0
- package/src/A.tsx +12 -0
- package/src/Area.tsx +15 -0
- package/src/RouteContext.ts +6 -0
- package/src/RouteProvider.tsx +30 -0
- package/src/TransientStateContext.ts +7 -0
- package/src/TransientStateProvider.tsx +40 -0
- package/src/URLContext.ts +6 -0
- package/src/URLProvider.tsx +28 -0
- package/src/types/AProps.ts +6 -0
- package/src/types/AreaProps.ts +6 -0
- package/src/types/EnhanceHref.ts +8 -0
- package/src/types/LinkNavigationProps.ts +8 -0
- package/src/types/RenderCallback.ts +4 -0
- package/src/types/TransientState.ts +6 -0
- package/src/useExternalState.ts +76 -0
- package/src/useLinkClick.ts +29 -0
- package/src/useNavigationComplete.ts +11 -0
- package/src/useNavigationStart.ts +9 -0
- package/src/useRoute.ts +19 -0
- package/src/useRouteLinks.ts +22 -0
- package/src/useRouteState.ts +56 -0
- package/src/useTransientState.ts +191 -0
- package/src/useURL.ts +9 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext, useEffect } from "react";
|
|
2
|
+
import type { EventCallback, NavigationOptions } from "stateshape";
|
|
3
|
+
import { RouteContext } from "./RouteContext.ts";
|
|
4
|
+
|
|
5
|
+
export function useNavigationComplete(
|
|
6
|
+
callback: EventCallback<NavigationOptions>,
|
|
7
|
+
) {
|
|
8
|
+
let route = useContext(RouteContext);
|
|
9
|
+
|
|
10
|
+
useEffect(() => route.on("navigationcomplete", callback), [route, callback]);
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useContext, useEffect } from "react";
|
|
2
|
+
import type { EventCallback, NavigationOptions } from "stateshape";
|
|
3
|
+
import { RouteContext } from "./RouteContext.ts";
|
|
4
|
+
|
|
5
|
+
export function useNavigationStart(callback: EventCallback<NavigationOptions>) {
|
|
6
|
+
let route = useContext(RouteContext);
|
|
7
|
+
|
|
8
|
+
useEffect(() => route.on("navigationstart", callback), [route, callback]);
|
|
9
|
+
}
|
package/src/useRoute.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useContext, useMemo } from "react";
|
|
2
|
+
import type { NavigationOptions } from "stateshape";
|
|
3
|
+
import { RouteContext } from "./RouteContext.ts";
|
|
4
|
+
import type { RenderCallback } from "./types/RenderCallback.ts";
|
|
5
|
+
import { useExternalState } from "./useExternalState.ts";
|
|
6
|
+
|
|
7
|
+
export function useRoute(callback?: RenderCallback<NavigationOptions>) {
|
|
8
|
+
let route = useContext(RouteContext);
|
|
9
|
+
|
|
10
|
+
useExternalState(route, callback, "navigation");
|
|
11
|
+
|
|
12
|
+
return useMemo(
|
|
13
|
+
() => ({
|
|
14
|
+
route,
|
|
15
|
+
at: route.at.bind(route),
|
|
16
|
+
}),
|
|
17
|
+
[route],
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type RefObject, useContext, useEffect } from "react";
|
|
2
|
+
import type { ContainerElement, Route } from "stateshape";
|
|
3
|
+
import { RouteContext } from "./RouteContext.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts plain HTML links to SPA route links.
|
|
7
|
+
*
|
|
8
|
+
* @param containerRef - A React Ref pointing to a container element.
|
|
9
|
+
* @param elements - An optional selector, or an HTML element, or a
|
|
10
|
+
* collection thereof, specifying the links inside the container to
|
|
11
|
+
* be converted to SPA route links. Default: `"a, area"`.
|
|
12
|
+
*/
|
|
13
|
+
export function useRouteLinks(
|
|
14
|
+
containerRef: RefObject<ContainerElement>,
|
|
15
|
+
elements?: Parameters<Route["observe"]>[1],
|
|
16
|
+
): void {
|
|
17
|
+
let route = useContext(RouteContext);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
return route.observe(() => containerRef.current, elements);
|
|
21
|
+
}, [route, elements, containerRef]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
compileURL,
|
|
4
|
+
type LocationValue,
|
|
5
|
+
type MatchState,
|
|
6
|
+
matchURL,
|
|
7
|
+
type NavigationOptions,
|
|
8
|
+
type URLData,
|
|
9
|
+
} from "stateshape";
|
|
10
|
+
import { useRoute } from "./useRoute.ts";
|
|
11
|
+
|
|
12
|
+
export type SetRouteState<T extends LocationValue> = (
|
|
13
|
+
update: URLData<T> | ((state: MatchState<T>) => URLData<T>),
|
|
14
|
+
) => void;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads and sets URL parameters in a way similar to React's `useState()`.
|
|
18
|
+
* This hooks returns `[state, setState]`, where `state` contains path
|
|
19
|
+
* placeholder parameters and query parameters, `{ params?, query? }`.
|
|
20
|
+
*
|
|
21
|
+
* Note that the path placeholders, `params`, are only available if the
|
|
22
|
+
* `url` parameter is an output of a typed URL builder (like the one
|
|
23
|
+
* produced with *url-shape*).
|
|
24
|
+
*/
|
|
25
|
+
export function useRouteState<T extends LocationValue>(
|
|
26
|
+
url?: T,
|
|
27
|
+
options?: Omit<NavigationOptions, "href">,
|
|
28
|
+
): [MatchState<T>, SetRouteState<T>] {
|
|
29
|
+
let { route } = useRoute();
|
|
30
|
+
|
|
31
|
+
let getState = useCallback(
|
|
32
|
+
(href?: T) => {
|
|
33
|
+
let resolvedHref = String(href ?? route.href);
|
|
34
|
+
|
|
35
|
+
return matchURL<T>(
|
|
36
|
+
url === undefined ? (resolvedHref as T) : url,
|
|
37
|
+
resolvedHref,
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
[url, route],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
let setState = useCallback<SetRouteState<T>>(
|
|
44
|
+
(update) => {
|
|
45
|
+
let urlData = typeof update === "function" ? update(getState()) : update;
|
|
46
|
+
|
|
47
|
+
route.navigate({
|
|
48
|
+
...options,
|
|
49
|
+
href: compileURL(url, urlData),
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
[url, route, options, getState],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return [getState(), setState];
|
|
56
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { isState, State } from "stateshape";
|
|
3
|
+
import { TransientStateContext } from "./TransientStateContext.ts";
|
|
4
|
+
import type { TransientState } from "./types/TransientState.ts";
|
|
5
|
+
import {
|
|
6
|
+
type SetExternalStateValue,
|
|
7
|
+
useExternalState,
|
|
8
|
+
} from "./useExternalState.ts";
|
|
9
|
+
|
|
10
|
+
function createState(
|
|
11
|
+
initial = true,
|
|
12
|
+
pending = false,
|
|
13
|
+
error?: unknown,
|
|
14
|
+
): TransientState {
|
|
15
|
+
return {
|
|
16
|
+
initial,
|
|
17
|
+
pending,
|
|
18
|
+
error,
|
|
19
|
+
time: Date.now(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TransientStateOptions = {
|
|
24
|
+
/**
|
|
25
|
+
* Whether to track the action state silently (e.g. with a background
|
|
26
|
+
* action or an optimistic update).
|
|
27
|
+
*
|
|
28
|
+
* When set to `true`, the state's `pending` property doesn't switch
|
|
29
|
+
* to `true` in the pending state.
|
|
30
|
+
*/
|
|
31
|
+
silent?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Delays switching the action state's `pending` property to `true`
|
|
34
|
+
* in the pending state by the given number of milliseconds.
|
|
35
|
+
*
|
|
36
|
+
* Use case: to avoid flashing a process indicator if the action is
|
|
37
|
+
* likely to complete by the end of a short delay.
|
|
38
|
+
*/
|
|
39
|
+
delay?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Allows the async action to reject explicitly, along with exposing
|
|
42
|
+
* the action state's `error` property that goes by default.
|
|
43
|
+
*/
|
|
44
|
+
throws?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ControllableTransientState = TransientState & {
|
|
48
|
+
update: SetExternalStateValue<TransientState>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The hook's `state` parameter is a unique string key or an instance of
|
|
53
|
+
* `State`. Providing a string key or a `State` instance allows to share the
|
|
54
|
+
* action state across multiple components. If the key is omitted or set to
|
|
55
|
+
* `null`, the action state stays locally scoped to the component where the
|
|
56
|
+
* hook is used.
|
|
57
|
+
*/
|
|
58
|
+
export function useTransientState<F extends (...args: unknown[]) => unknown>(
|
|
59
|
+
state: string | State<TransientState> | null,
|
|
60
|
+
action: F,
|
|
61
|
+
): [
|
|
62
|
+
ControllableTransientState,
|
|
63
|
+
(...args: [...Parameters<F>, TransientStateOptions?]) => ReturnType<F>,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export function useTransientState(
|
|
67
|
+
state: string | State<TransientState> | null,
|
|
68
|
+
): [ControllableTransientState];
|
|
69
|
+
|
|
70
|
+
export function useTransientState<F extends (...args: unknown[]) => unknown>(
|
|
71
|
+
state: string | State<TransientState> | null,
|
|
72
|
+
action?: F,
|
|
73
|
+
): [
|
|
74
|
+
ControllableTransientState,
|
|
75
|
+
((...args: [...Parameters<F>, TransientStateOptions?]) => ReturnType<F>)?,
|
|
76
|
+
] {
|
|
77
|
+
let stateMap = useContext(TransientStateContext);
|
|
78
|
+
let stateRef = useRef<State<TransientState> | null>(null);
|
|
79
|
+
let [stateItemInited, setStateItemInited] = useState(false);
|
|
80
|
+
|
|
81
|
+
let resolvedState = useMemo(() => {
|
|
82
|
+
if (isState<TransientState>(state)) return state;
|
|
83
|
+
|
|
84
|
+
if (typeof state === "string") {
|
|
85
|
+
let stateItem = stateMap.get(state);
|
|
86
|
+
|
|
87
|
+
if (!stateItem) {
|
|
88
|
+
stateItem = new State(createState());
|
|
89
|
+
stateMap.set(state, stateItem);
|
|
90
|
+
|
|
91
|
+
if (!stateItemInited) setStateItemInited(true);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return stateItem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!stateRef.current) stateRef.current = new State(createState());
|
|
98
|
+
|
|
99
|
+
return stateRef.current;
|
|
100
|
+
}, [state, stateMap, stateItemInited]);
|
|
101
|
+
|
|
102
|
+
let [actionState, setActionState] = useExternalState(resolvedState);
|
|
103
|
+
|
|
104
|
+
let trackableAction = useCallback(
|
|
105
|
+
(...args: [...Parameters<F>, TransientStateOptions?]) => {
|
|
106
|
+
if (!action)
|
|
107
|
+
throw new Error(
|
|
108
|
+
"A trackable action is only available when the hook's 'action' parameter is set",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
let options = args.at(-1) as TransientStateOptions | undefined;
|
|
112
|
+
let originalArgs = args.slice(0, -1) as Parameters<F>;
|
|
113
|
+
let result: unknown;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
result = action(...originalArgs);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
setActionState((prevState) => ({
|
|
119
|
+
...prevState,
|
|
120
|
+
...createState(false, false, error),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
if (options?.throws) throw error;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (result instanceof Promise) {
|
|
127
|
+
let delayedTracking: ReturnType<typeof setTimeout> | null = null;
|
|
128
|
+
|
|
129
|
+
if (!options?.silent) {
|
|
130
|
+
let delay = options?.delay;
|
|
131
|
+
|
|
132
|
+
if (delay === undefined)
|
|
133
|
+
setActionState((prevState) => ({
|
|
134
|
+
...prevState,
|
|
135
|
+
...createState(false, true),
|
|
136
|
+
}));
|
|
137
|
+
else
|
|
138
|
+
delayedTracking = setTimeout(() => {
|
|
139
|
+
setActionState((prevState) => ({
|
|
140
|
+
...prevState,
|
|
141
|
+
...createState(false, true),
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
delayedTracking = null;
|
|
145
|
+
}, delay);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
.then((value) => {
|
|
150
|
+
if (delayedTracking !== null) clearTimeout(delayedTracking);
|
|
151
|
+
|
|
152
|
+
setActionState((prevState) => ({
|
|
153
|
+
...prevState,
|
|
154
|
+
...createState(false, false),
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
return value;
|
|
158
|
+
})
|
|
159
|
+
.catch((error) => {
|
|
160
|
+
if (delayedTracking !== null) clearTimeout(delayedTracking);
|
|
161
|
+
|
|
162
|
+
setActionState((prevState) => ({
|
|
163
|
+
...prevState,
|
|
164
|
+
...createState(false, false, error),
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
if (options?.throws) throw error;
|
|
168
|
+
}) as ReturnType<F>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setActionState((prevState) => ({
|
|
172
|
+
...prevState,
|
|
173
|
+
...createState(false, false),
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
return result as ReturnType<F>;
|
|
177
|
+
},
|
|
178
|
+
[action, setActionState],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return useMemo(() => {
|
|
182
|
+
let extendedActionState = {
|
|
183
|
+
...actionState,
|
|
184
|
+
update: setActionState,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return action
|
|
188
|
+
? [extendedActionState, trackableAction]
|
|
189
|
+
: [extendedActionState];
|
|
190
|
+
}, [action, trackableAction, actionState, setActionState]);
|
|
191
|
+
}
|
package/src/useURL.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import type { NavigationOptions } from "stateshape";
|
|
3
|
+
import type { RenderCallback } from "./types/RenderCallback.ts";
|
|
4
|
+
import { URLContext } from "./URLContext.ts";
|
|
5
|
+
import { useExternalState } from "./useExternalState.ts";
|
|
6
|
+
|
|
7
|
+
export function useURL(callback?: RenderCallback<NavigationOptions>) {
|
|
8
|
+
return useExternalState(useContext(URLContext), callback, "navigation");
|
|
9
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"include": ["./index.ts", "./playwright.config.ts", "./tests", "./src"],
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"emitDeclarationOnly": true,
|
|
6
|
+
"lib": ["ESNext", "DOM"],
|
|
7
|
+
"target": "esnext",
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"module": "nodenext",
|
|
10
|
+
"moduleResolution": "nodenext",
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true
|
|
16
|
+
}
|
|
17
|
+
}
|