diff --git a/clients/typescript/README.md b/clients/typescript/README.md
index 9aec225..da6b8b2 100644
--- a/clients/typescript/README.md
+++ b/clients/typescript/README.md
@@ -42,7 +42,12 @@ const token = client.auth.token;
## React
```tsx
-import { PicloudProvider, useTopic, useEndpoint } from '@picloud/client/react';
+import {
+ PicloudProvider,
+ useTopic,
+ useEndpointGet,
+ useEndpointPost
+} from '@picloud/client/react';
// Wrap your tree once: …
@@ -52,11 +57,25 @@ function ChatRoom({ roomId }: { roomId: string }) {
}
function UserProfile({ id }: { id: string }) {
- const { data, loading, error } = useEndpoint(`/api/users/${id}`).get();
+ // Auto-fires when `id` changes; conforms to Rules of Hooks.
+ const { data, loading, error } = useEndpointGet(`/api/users/${id}`);
if (loading) return ;
if (error) return ;
return {data?.name}
;
}
+
+function CreateUserForm() {
+ // Event-driven: NOT fired on mount; call `mutate(body)` on submit.
+ const { mutate, loading, error } = useEndpointPost(
+ '/api/users'
+ );
+ return (
+
+ );
+}
```
## Svelte
diff --git a/clients/typescript/package-lock.json b/clients/typescript/package-lock.json
index 92cec06..ea4ea61 100644
--- a/clients/typescript/package-lock.json
+++ b/clients/typescript/package-lock.json
@@ -184,6 +184,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -207,6 +208,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -1048,6 +1050,7 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -1117,6 +1120,7 @@
"integrity": "sha512-3ek6mwJL5/VBewBcY4S66cqlCtK3qi4WIq37Z0m/NHw1hjhI7274Mx1qz/+ggSzyBCOEf7eHjBN6INjPAWYfYw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -1685,6 +1689,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -2000,6 +2005,7 @@
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssstyle": "^4.1.0",
"data-urls": "^5.0.0",
@@ -2282,6 +2288,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -2331,6 +2338,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
@@ -2414,6 +2422,7 @@
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -2427,6 +2436,7 @@
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -2851,6 +2861,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -2872,6 +2883,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
diff --git a/clients/typescript/src/react/index.ts b/clients/typescript/src/react/index.ts
index 0ae8026..797b458 100644
--- a/clients/typescript/src/react/index.ts
+++ b/clients/typescript/src/react/index.ts
@@ -60,26 +60,20 @@ export interface QueryState {
error: unknown;
}
-export interface EndpointHook {
- get: () => QueryState;
- post: (body?: Req) => QueryState;
+export interface MutationState {
+ data: T | null;
+ loading: boolean;
+ error: unknown;
}
/**
- * Typed endpoint hook. `useEndpoint(path).get()` fires a GET and
- * returns `{ data, loading, error }`, re-running when `path` changes.
- * `.post(body)` is the mutation variant (auto-fires once per mount).
+ * F-T-001: flat GET hook. Replaces `useEndpoint(path).get()` which
+ * violated the Rules of Hooks (calling `useState`/`useEffect` from
+ * inside a returned function). Auto-fires once per `path` change and
+ * re-runs on mount.
*/
-export function useEndpoint(path: string): EndpointHook {
+export function useEndpointGet(path: string): QueryState {
const client = usePicloud();
- return {
- get: () => useResource(() => client.endpoint(path).get(), path, 'GET'),
- post: (body?: Req) =>
- useResource(() => client.endpoint(path).post(body), path, 'POST')
- };
-}
-
-function useResource(run: () => Promise, key: string, method: string): QueryState {
const [state, setState] = useState>({
data: null,
loading: true,
@@ -88,14 +82,42 @@ function useResource(run: () => Promise, key: string, method: string):
useEffect(() => {
let active = true;
setState({ data: null, loading: true, error: null });
- run()
+ client
+ .endpoint(path)
+ .get()
.then((data) => active && setState({ data, loading: false, error: null }))
.catch((error) => active && setState({ data: null, loading: false, error }));
return () => {
active = false;
};
- // `run` is recreated each render; key it on path + method instead.
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [key, method]);
+ }, [client, path]);
return state;
}
+
+/**
+ * F-T-001 + F-T-002: event-driven POST hook. Returns
+ * `{ mutate, data, loading, error }` — `mutate(body)` fires the POST.
+ * Does NOT auto-fire on mount (the previous `useEndpoint(path).post()`
+ * shape would create a user / send an email on every render-refresh).
+ */
+export function useEndpointPost(
+ path: string
+): MutationState & { mutate: (body?: Req) => Promise } {
+ const client = usePicloud();
+ const [state, setState] = useState>({
+ data: null,
+ loading: false,
+ error: null
+ });
+ const mutate = async (body?: Req) => {
+ setState({ data: null, loading: true, error: null });
+ try {
+ const data = await client.endpoint(path).post(body);
+ setState({ data, loading: false, error: null });
+ } catch (error) {
+ setState({ data: null, loading: false, error });
+ }
+ };
+ return { ...state, mutate };
+}