@usegraft/sdk-react 1.0.0-beta.1 → 1.0.0-beta.3

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/dist/index.d.ts CHANGED
@@ -22,7 +22,10 @@ interface Graft<TCollections extends Record<string, AnyCollection>> {
22
22
  getContent<K extends keyof TCollections & string>(collection: K, slug: string, options?: ReadOptions): Promise<Document<TCollections[K]> | null>;
23
23
  /** Typed listDocuments. */
24
24
  listContent<K extends keyof TCollections & string>(collection: K, options?: ListOptions): Promise<Document<TCollections[K]>[]>;
25
- /** Typed searchDocuments (full-text, best-ranked first). */
25
+ /**
26
+ * Typed searchDocuments (full-text, best-ranked first). A blank query
27
+ * resolves to [] without a request.
28
+ */
26
29
  searchContent<K extends keyof TCollections & string>(collection: K, query: string, options?: SearchOptions): Promise<SearchHit<TCollections[K]>[]>;
27
30
  /** The underlying sdk-core client, for anything the helpers don't cover. */
28
31
  client: GraftClient<TCollections>;
@@ -46,7 +49,9 @@ declare function createGraft<TCollections extends Record<string, AnyCollection>>
46
49
  * plain async function and composes with either of them.
47
50
  *
48
51
  * Browser-side. `useEffect` does not run during server rendering, so on the
49
- * server these hooks render their loading state and nothing else. Data that
52
+ * server these hooks render their loading state and nothing else. The one
53
+ * exception is a blank search, which is answered without a read, so it renders
54
+ * its empty result on the server too. Data that
50
55
  * has to be in the HTML belongs in a loader or a server adapter
51
56
  * (@usegraft/sdk-react-router, @usegraft/sdk-tanstack-start, and the rest).
52
57
  *
@@ -85,7 +90,10 @@ interface GraftHooks<TCollections extends Record<string, AnyCollection>> {
85
90
  useContent: <K extends keyof TCollections & string>(collection: K, slug: string, options?: ReadOptions) => AsyncState<Document<TCollections[K]> | null>;
86
91
  /** A collection, in index order. */
87
92
  useContentList: <K extends keyof TCollections & string>(collection: K, options?: ListOptions) => AsyncState<Document<TCollections[K]>[]>;
88
- /** Full-text search within one collection, best-ranked first. */
93
+ /**
94
+ * Full-text search within one collection, best-ranked first. A blank query
95
+ * reports no hits and sends no request.
96
+ */
89
97
  useContentSearch: <K extends keyof TCollections & string>(collection: K, query: string, options?: SearchOptions) => AsyncState<SearchHit<TCollections[K]>[]>;
90
98
  }
91
99
  /**
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ function createGraft(options) {
57
57
  },
58
58
  async searchContent(collection, query, opts) {
59
59
  assertNoBranch(opts);
60
+ if (query.trim() === "") return [];
60
61
  return client.searchDocuments(collection, query, opts);
61
62
  }
62
63
  };
@@ -72,29 +73,41 @@ import {
72
73
  } from "react";
73
74
  import { jsx } from "react/jsx-runtime";
74
75
  var PENDING = { data: void 0, error: void 0, loading: true };
76
+ var NO_HITS = {
77
+ data: Object.freeze([]),
78
+ error: void 0,
79
+ loading: false
80
+ };
75
81
  function asError(cause) {
76
82
  return cause instanceof Error ? cause : new Error(String(cause));
77
83
  }
78
- function useRead(read) {
84
+ function useRead(read, settled) {
79
85
  const [attempt, setAttempt] = useState(0);
80
- const [state, setState] = useState(PENDING);
86
+ const [answer, setAnswer] = useState();
81
87
  useEffect(() => {
88
+ if (settled !== void 0) return;
82
89
  let live = true;
83
- setState(PENDING);
84
90
  read().then(
85
91
  (data) => {
86
- if (live) setState({ data, error: void 0, loading: false });
92
+ if (live) setAnswer({ read, attempt, state: { data, error: void 0, loading: false } });
87
93
  },
88
94
  (cause) => {
89
- if (live) setState({ data: void 0, error: asError(cause), loading: false });
95
+ if (live) {
96
+ setAnswer({
97
+ read,
98
+ attempt,
99
+ state: { data: void 0, error: asError(cause), loading: false }
100
+ });
101
+ }
90
102
  }
91
103
  );
92
104
  return () => {
93
105
  live = false;
94
106
  };
95
- }, [read, attempt]);
107
+ }, [read, attempt, settled]);
96
108
  const refresh = useCallback(() => setAttempt((previous) => previous + 1), []);
97
- return { ...state, refresh };
109
+ const current = answer?.read === read && answer.attempt === attempt ? answer.state : PENDING;
110
+ return { ...settled ?? current, refresh };
98
111
  }
99
112
  function createGraftHooks(graft) {
100
113
  const GraftContext = createContext(graft);
@@ -136,7 +149,8 @@ function createGraftHooks(graft) {
136
149
  () => handle.searchContent(collection, query, { branch, limit }),
137
150
  [handle, collection, query, branch, limit]
138
151
  );
139
- return useRead(read);
152
+ const settled = query.trim() === "" && branch === void 0 ? NO_HITS : void 0;
153
+ return useRead(read, settled);
140
154
  }
141
155
  };
142
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usegraft/sdk-react",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
4
  "description": "React browser client: typed content reads and hooks over Graft's read-only content API.",
5
5
  "keywords": [
6
6
  "agent",
@@ -38,15 +38,15 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@usegraft/content-api": "1.0.0-beta.1",
42
- "@usegraft/contracts": "1.0.0-beta.1",
43
- "@usegraft/sdk-core": "1.0.0-beta.1"
41
+ "@usegraft/content-api": "1.0.0-beta.3",
42
+ "@usegraft/contracts": "1.0.0-beta.3",
43
+ "@usegraft/sdk-core": "1.0.0-beta.3"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@testing-library/react": "^16.3.3",
47
47
  "@types/react": "^19.2.18",
48
48
  "@types/react-dom": "^19.2.5",
49
- "@usegraft/core": "1.0.0-beta.1",
49
+ "@usegraft/core": "1.0.0-beta.3",
50
50
  "jsdom": "^25.0.1",
51
51
  "react": "^19.2.8",
52
52
  "react-dom": "^19.2.8"