@mittwald/react-ghostmaker 1.0.3 → 1.1.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/README.md CHANGED
@@ -7,10 +7,6 @@ models that seamlessly integrate with React Suspense and TanStack Query. These
7
7
  ghosts automatically handle async operations, caching, and loading states,
8
8
  making your code cleaner and more declarative.
9
9
 
10
- **🔑 Say goodbye to query key management!** Ghost Maker automatically generates
11
- and manages TanStack Query keys based on your method chains, so you can focus on
12
- your business logic instead of cache coordination.
13
-
14
10
  ## 📖 Table of Contents
15
11
 
16
12
  - [✨ Features](#-features)
@@ -28,7 +24,9 @@ your business logic instead of cache coordination.
28
24
 
29
25
  ## ✨ Features
30
26
 
31
- - 🎭 **Ghost Proxies**: Transform any object into a suspense-ready ghost
27
+ 🎭 **Ghost Proxies**: Transform any object—models, existing API clients,
28
+ services, or utilities into a suspense-ready ghost
29
+
32
30
  - ⚡ **Lazy Execution**: Ghosts don't execute until `.use()` or `.render()` is
33
31
  called
34
32
  - 🎯 **Precise Loading States**: Control exactly where Suspense boundaries
@@ -171,18 +169,38 @@ function TraditionalApp() {
171
169
 
172
170
  ### Creating Your First Ghost
173
171
 
172
+ React Ghost Maker can turn any object into a ghost—not just domain models, but
173
+ also existing API clients, service objects, or utility classes. This makes it
174
+ easy to add Suspense and caching to legacy code or third-party libraries.
175
+
174
176
  ```tsx
175
177
  import { makeGhost } from "@mittwald/react-ghostmaker";
176
178
  import { Suspense } from "react";
177
179
 
178
- // Your domain model
180
+ // Example: Wrapping an API client
181
+ class BlogApiClient {
182
+ async fetchBlog(id: string) {
183
+ const response = await fetch(`/api/blogs/${id}`);
184
+ return response.json();
185
+ }
186
+ }
187
+
188
+ const BlogApiGhost = makeGhost(new BlogApiClient());
189
+
190
+ function BlogView() {
191
+ // Use ghostified API client
192
+ const blog = BlogApiGhost.fetchBlog("123").use();
193
+ return <article>{blog.title}</article>;
194
+ }
195
+
196
+ // ...existing code...
197
+
198
+ // You can also use domain models as shown below:
179
199
  class Blog {
180
200
  constructor(public id: string) {}
181
-
182
201
  static ofId(id: string): Blog {
183
202
  return new Blog(id);
184
203
  }
185
-
186
204
  async getDetailed() {
187
205
  const response = await fetch(`/api/blogs/${this.id}`);
188
206
  const data = await response.json();
@@ -196,23 +214,18 @@ class DetailedBlog extends Blog {
196
214
  this.title = data.title;
197
215
  this.author = data.author;
198
216
  }
199
-
200
217
  public readonly title: string;
201
218
  public readonly author: string;
202
219
  }
203
220
 
204
- // Create a ghost
205
221
  const BlogGhost = makeGhost(Blog);
206
222
 
207
- // Use in React components
208
- function BlogView() {
223
+ function BlogViewModel() {
209
224
  const blogGhost = BlogGhost.ofId("123");
210
-
211
225
  const { value: blogTitle, invalidate } = blogGhost
212
226
  .getDetailed()
213
227
  .title.transform((title) => title.toUpperCase())
214
228
  .useGhost();
215
-
216
229
  return (
217
230
  <article>
218
231
  <h2>{blogTitle}</h2>
@@ -226,6 +239,7 @@ function App() {
226
239
  return (
227
240
  <Suspense fallback={<div>Loading blog...</div>}>
228
241
  <BlogView />
242
+ <BlogViewModel />
229
243
  </Suspense>
230
244
  );
231
245
  }
@@ -1,18 +1,21 @@
1
1
  import is from "@sindresorhus/is";
2
2
  import { modelIdentifiers } from "./modelIdentifier";
3
3
  import { hashObject } from "./hash";
4
- const getTargetQueryKey = (something) => {
5
- if (is.primitive(something)) {
6
- return String(something);
7
- }
4
+ const getObjectName = (something) => {
8
5
  const isObject = is.object(something);
9
6
  const isClass = is.class(something);
10
7
  const isFunction = is.function(something);
11
- const objectName = isFunction
8
+ return isFunction
12
9
  ? something.name
13
10
  : isClass || isObject
14
11
  ? something.constructor.name
15
12
  : "unknown";
13
+ };
14
+ const getTargetQueryKey = (something) => {
15
+ if (is.primitive(something)) {
16
+ return String(something);
17
+ }
18
+ const objectName = getObjectName(something);
16
19
  const thisModelIdentifiers = modelIdentifiers
17
20
  .map((fn) => fn(something))
18
21
  .filter(is.string)
@@ -26,7 +29,7 @@ const getArgKey = (arg) => {
26
29
  if (is.primitive(arg)) {
27
30
  return String(arg);
28
31
  }
29
- return hashObject(arg);
32
+ return `${getObjectName(arg)}(hash:${hashObject(arg)})`;
30
33
  };
31
34
  export const queries = {
32
35
  ghostmaker: () => ["react-ghostmaker"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/react-ghostmaker",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "author": "Mittwald CM Service GmbH & Co. KG <opensource@mittwald.de>",
5
5
  "contributors": [
6
6
  "Marco Falkenberg <m.falkenberg@mittwald.de>"
@@ -41,19 +41,19 @@
41
41
  },
42
42
  "devDependencies": {
43
43
  "@eslint/eslintrc": "^3.3.1",
44
- "@eslint/js": "^9.37.0",
44
+ "@eslint/js": "^9.39.1",
45
45
  "@tanstack/react-query": "^5.90.5",
46
46
  "@testing-library/dom": "^10.4.1",
47
47
  "@testing-library/react": "^16.3.0",
48
48
  "@types/react": "^19.2.2",
49
49
  "@types/react-dom": "^19.2.2",
50
- "@typescript-eslint/eslint-plugin": "^8.46.1",
50
+ "@typescript-eslint/eslint-plugin": "^8.46.3",
51
51
  "@typescript-eslint/parser": "^8.46.1",
52
52
  "eslint": "^9.37.0",
53
53
  "eslint-config-prettier": "^10.1.8",
54
54
  "eslint-plugin-json": "^4.0.1",
55
55
  "eslint-plugin-prettier": "^5.5.4",
56
- "globals": "^16.4.0",
56
+ "globals": "^16.5.0",
57
57
  "happy-dom": "^20.0.2",
58
58
  "license-checker-rseidelsohn": "^4.4.2",
59
59
  "prettier": "^3.6.2",