@pylonsync/react 0.4.15 → 0.4.17

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/dynamic.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ComponentType } from "react";
1
+ import React, { type ComponentType } from "react";
2
2
  export interface DynamicOptions {
3
3
  /** Rendered while the chunk loads (and, when `ssr` is false, on the server). */
4
4
  loading?: ComponentType<any> | null;
@@ -24,6 +24,14 @@ type Loader<P> = () => Promise<{
24
24
  * The returned component renders `loading` until the chunk arrives, then the
25
25
  * real one. Call it at MODULE level, not inside a render — a `dynamic()` call
26
26
  * per render creates a new component type each time and remounts the subtree.
27
+ *
28
+ * `ref` reaches the loaded component: React 19 treats it as an ordinary prop,
29
+ * so the spread carries it to a `forwardRef` target's imperative handle. That
30
+ * matters for the exact components people defer — an editor whose save path
31
+ * calls `ref.current.export()` would fail silently otherwise — so it's covered
32
+ * by a test rather than left to React's prop semantics staying put.
27
33
  */
28
- export declare function dynamic<P extends object>(loader: Loader<P>, options?: DynamicOptions): ComponentType<P>;
34
+ export declare function dynamic<P extends object>(loader: Loader<P>, options?: DynamicOptions): ComponentType<P & {
35
+ ref?: React.Ref<any>;
36
+ }>;
29
37
  export {};
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.15",
6
+ "version": "0.4.17",
7
7
  "type": "module",
8
8
  "main": "./src/index.ts",
9
9
  "types": "./dist/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  "prepack": "bun run build"
15
15
  },
16
16
  "dependencies": {
17
- "@pylonsync/sdk": "0.4.15",
18
- "@pylonsync/sync": "0.4.15"
17
+ "@pylonsync/sdk": "0.4.17",
18
+ "@pylonsync/sync": "0.4.17"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "react": ">=19.0.0"
@@ -108,3 +108,24 @@ describe("dynamic() with ssr:true", () => {
108
108
  await waitFor(() => expect(screen.getByTestId("heavy")).toBeDefined());
109
109
  });
110
110
  });
111
+
112
+ describe("dynamic() and refs", () => {
113
+ test("a ref reaches the loaded component's imperative handle", async () => {
114
+ // React 19 passes `ref` as an ordinary prop, so the spread carries it —
115
+ // but that's incidental enough to be worth pinning: a deferred editor
116
+ // whose save path calls ref.current.export() breaks silently otherwise.
117
+ const Editor = React.forwardRef<{ save: () => string }, { doc: string }>(
118
+ (props, ref) => {
119
+ React.useImperativeHandle(ref, () => ({ save: () => `saved:${props.doc}` }));
120
+ return <div data-testid="ed">{props.doc}</div>;
121
+ },
122
+ );
123
+ const D = dynamic<{ doc: string }>(async () => ({ default: Editor as any }), {
124
+ loading: Skeleton,
125
+ });
126
+ const ref = React.createRef<{ save: () => string }>();
127
+ render(<D doc="hello" ref={ref} />);
128
+ await waitFor(() => expect(screen.getByTestId("ed")).toBeDefined());
129
+ expect(ref.current?.save()).toBe("saved:hello");
130
+ });
131
+ });
package/src/dynamic.tsx CHANGED
@@ -59,11 +59,17 @@ function resolveComponent<P>(
59
59
  * The returned component renders `loading` until the chunk arrives, then the
60
60
  * real one. Call it at MODULE level, not inside a render — a `dynamic()` call
61
61
  * per render creates a new component type each time and remounts the subtree.
62
+ *
63
+ * `ref` reaches the loaded component: React 19 treats it as an ordinary prop,
64
+ * so the spread carries it to a `forwardRef` target's imperative handle. That
65
+ * matters for the exact components people defer — an editor whose save path
66
+ * calls `ref.current.export()` would fail silently otherwise — so it's covered
67
+ * by a test rather than left to React's prop semantics staying put.
62
68
  */
63
69
  export function dynamic<P extends object>(
64
70
  loader: Loader<P>,
65
71
  options: DynamicOptions = {},
66
- ): ComponentType<P> {
72
+ ): ComponentType<P & { ref?: React.Ref<any> }> {
67
73
  const { loading: Loading = null, ssr = false } = options;
68
74
 
69
75
  if (ssr) {
@@ -81,7 +87,7 @@ export function dynamic<P extends object>(
81
87
  </Suspense>
82
88
  );
83
89
  WithSuspense.displayName = "DynamicSSR";
84
- return WithSuspense as ComponentType<P>;
90
+ return WithSuspense as ComponentType<P & { ref?: React.Ref<any> }>;
85
91
  }
86
92
 
87
93
  // Client-only. The fallback renders on the server AND on the first client
@@ -117,5 +123,5 @@ export function dynamic<P extends object>(
117
123
  return <Loaded {...(props as any)} />;
118
124
  };
119
125
  ClientOnly.displayName = "Dynamic";
120
- return ClientOnly as ComponentType<P>;
126
+ return ClientOnly as ComponentType<P & { ref?: React.Ref<any> }>;
121
127
  }