Skip to content

Commit cf8a09a

Browse files
committed
BREAKING: fix typechecking for async components
This change is unfortunately necessary until either microsoft/TypeScript#61620 (a new issue reported by me) or microsoft/TypeScript#21699 (a longstanding old issue unlikely to be fixed) are resolved. TypeScript uses the JSX.Element type to determine the type of any JSX expression. It is not possible for two different JSX expressions to evaluate to different types. Gooey supports asynchronous components, **but** in order for this to work a component must be defined as something that returns a JSX.Element; so JSX.Element must be extended to become: RenderNode | (Promise<RenderNode> & Partial<RenderNode>) This extension of (Promise<RenderNode> & Partial<RenderNode>) means that in practice, users can easily call rendernode methods (like `.retain()`) via: const jsx = <div />; jsx.retain?.(); // ... jsx.release?.(); Which should always work, as the `createElement` jsxFactory function always returns a `RenderNode`. It's really unfortunate that the optional chaining operation is needed here. Hopefully when either of those issues are fixed, the JSX evaluation type can be separated from the function component return type.
1 parent 9e024bd commit cf8a09a

File tree

7 files changed

+35
-35
lines changed

7 files changed

+35
-35
lines changed

src/demos/garbage-test/garbage-test.tsx

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import type {
2-
Component} from '../../index';
1+
import type { Component } from '../../index';
32
import Gooey, {
4-
mount,
5-
collection,
6-
model,
73
calc,
8-
subscribe,
4+
collection,
95
flush,
106
IntrinsicObserver,
7+
model,
8+
mount,
9+
subscribe,
1110
} from '../../index';
1211

1312
// eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -463,7 +462,7 @@ const App = () => (
463462
const components = strings.map((item) => (
464463
<MyComponent name={item} />
465464
));
466-
components.forEach((component) => component.retain());
465+
components.forEach((component) => component.retain?.());
467466
const unmount = mount(
468467
el,
469468
<>{calc(() => state.isMounted && components)}</>
@@ -479,7 +478,7 @@ const App = () => (
479478
destroy: () => {
480479
unmount();
481480
components.forEach((component) =>
482-
component.release()
481+
component.release?.()
483482
);
484483
},
485484
};

src/demos/teleportation/teleportation.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { Component} from '../../index';
2-
import Gooey, { mount, model, calc } from '../../index';
1+
import type { Component } from '../../index';
2+
import Gooey, { calc, model, mount } from '../../index';
33

44
const appRoot = document.getElementById('app');
55
if (!appRoot) {
@@ -34,12 +34,12 @@ const Example: Component<{ children: JSX.Element }> = (
3434
left: false,
3535
});
3636
onMount(() => {
37-
children.retain();
37+
children.retain?.();
3838
const handle = setInterval(() => {
3939
state.left = !state.left;
4040
}, 3000);
4141
return () => {
42-
children.release();
42+
children.release?.();
4343
clearInterval(handle);
4444
};
4545
});

src/viewcontroller/jsx.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ declare global {
5252
/**
5353
* The core type produced by a JSX expression
5454
*/
55-
type Element = RenderNode;
55+
type Element = RenderNode | (Promise<RenderNode> & Partial<RenderNode>);
5656

5757
/**
5858
* The core type allowable as a child node in a JSX expression

src/viewcontroller/mount.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as log from '../common/log';
22
import { flush } from '../model/engine';
3+
import { renderJSXNode } from './renderjsx';
34
import { ArrayRenderNode } from './rendernode/arrayrendernode';
45
import { ForeignRenderNode } from './rendernode/foreignrendernode';
56
import { PortalRenderNode } from './rendernode/portalrendernode';
@@ -8,14 +9,14 @@ import { HTML_NAMESPACE } from './xmlnamespace';
89

910
export function mount(
1011
target: Element | ShadowRoot,
11-
node: RenderNode
12+
node: JSX.Node
1213
): () => void {
1314
const skipNodes = target.childNodes.length;
1415
const children: RenderNode[] = [];
1516
for (let i = 0; i < target.childNodes.length; ++i) {
1617
children.push(ForeignRenderNode(target.childNodes[i]));
1718
}
18-
children.push(node);
19+
children.push(renderJSXNode(node));
1920
const root = PortalRenderNode(
2021
target,
2122
ArrayRenderNode(children),

src/viewcontroller/rendernode/componentrendernode.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function ComponentRenderNode<TProps>(
6666
let onUnmountCallbacks: undefined | (() => void)[];
6767
let onDestroyCallbacks: undefined | (() => void)[];
6868
let owned: Set<Retainable> = new Set();
69-
let errorHandler: undefined | ((e: Error) => RenderNode | null);
69+
let errorHandler: undefined | ((e: Error) => JSX.Element | null);
7070

7171
function ensureResult() {
7272
if (!result) {
@@ -96,7 +96,7 @@ export function ComponentRenderNode<TProps>(
9696
if (!onDestroyCallbacks) onDestroyCallbacks = [];
9797
onDestroyCallbacks.push(handler);
9898
},
99-
onError: (handler: (e: Error) => RenderNode | null) => {
99+
onError: (handler: (e: Error) => JSX.Element | null) => {
100100
log.assert(
101101
callbacksAllowed,
102102
'onError must be called in component body'
@@ -116,7 +116,7 @@ export function ComponentRenderNode<TProps>(
116116
} else {
117117
componentProps = props ? { ...props, children } : { children };
118118
}
119-
let jsxResult: RenderNode | Error;
119+
let jsxResult: JSX.Element | Error;
120120
try {
121121
jsxResult =
122122
Component(componentProps, lifecycle) || emptyRenderNode;

src/viewcontroller/rendernode/webcomponentrendernode.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function WebComponentRenderNode<
134134
let onUnmountCallbacks: undefined | (() => void)[];
135135
let onDestroyCallbacks: undefined | (() => void)[];
136136
const owned: Set<Retainable> = new Set();
137-
let errorHandler: ((e: Error) => RenderNode | null) | undefined;
137+
let errorHandler: ((e: Error) => JSX.Element | null) | undefined;
138138

139139
function ensureResult() {
140140
if (!result) {
@@ -164,7 +164,7 @@ export function WebComponentRenderNode<
164164
if (!onDestroyCallbacks) onDestroyCallbacks = [];
165165
onDestroyCallbacks.push(handler);
166166
},
167-
onError: (handler: (e: Error) => RenderNode | null) => {
167+
onError: (handler: (e: Error) => JSX.Element | null) => {
168168
log.assert(
169169
callbacksAllowed,
170170
'onError must be called in component body'
@@ -298,7 +298,7 @@ export function WebComponentRenderNode<
298298
...fields,
299299
};
300300
const Component = options.Component;
301-
let jsxResult: RenderNode | Error;
301+
let jsxResult: JSX.Element | Error;
302302
try {
303303
jsxResult =
304304
Component(componentProps, lifecycle) || emptyRenderNode;

src/viewcontroller/view.test.tsx

+14-14
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ suite('mount static', () => {
495495
hello
496496
</div>
497497
);
498-
const dest = source.clone({ class: 'dest', 'data-cloned': 'true' }, [
498+
const dest = source.clone?.({ class: 'dest', 'data-cloned': 'true' }, [
499499
<>howdy</>,
500500
]);
501501
mount(
@@ -555,7 +555,7 @@ suite('mount static', () => {
555555
</span>
556556
</div>
557557
);
558-
const dest = source.clone();
558+
const dest = source.clone?.();
559559
mount(
560560
testRoot,
561561
<>
@@ -763,7 +763,7 @@ suite('mount calculations', () => {
763763
{calc(() => state.content)}
764764
</div>
765765
);
766-
jsx.retain();
766+
jsx.retain?.();
767767
const unmount = mount(testRoot, jsx);
768768
const divEl = testRoot.querySelector('#ok');
769769
unmount();
@@ -903,7 +903,7 @@ suite('mount components', () => {
903903
};
904904

905905
const jsx = <Greet />;
906-
jsx.retain();
906+
jsx.retain?.();
907907

908908
let unmount = mount(testRoot, jsx);
909909
assert.deepEqual(['render', 'onMount:1'], sequence);
@@ -1339,7 +1339,7 @@ suite('mount class components', () => {
13391339
}
13401340

13411341
const jsx = <Greet />;
1342-
jsx.retain();
1342+
jsx.retain?.();
13431343

13441344
let unmount = mount(testRoot, jsx);
13451345
assert.deepEqual(['construct', 'render', 'onMount:1'], sequence);
@@ -2307,7 +2307,7 @@ suite('mount collection mapped view', () => {
23072307
const jsx = <>{items.mapView((item) => calc(() => <>{item}</>))}</>;
23082308
const unmount = mount(testRoot, jsx);
23092309
assert.is('foobarbazbum', testRoot.textContent);
2310-
jsx.retain();
2310+
jsx.retain?.();
23112311
unmount();
23122312
items.shift();
23132313
items.pop();
@@ -2325,7 +2325,7 @@ suite('mount collection mapped view', () => {
23252325
const jsx = <>{items.mapView((item) => calc(() => <>{item}</>))}</>;
23262326
const unmount = mount(testRoot, jsx);
23272327
assert.is('foobarbazbum', testRoot.textContent);
2328-
jsx.retain();
2328+
jsx.retain?.();
23292329
unmount();
23302330
items.shift();
23312331
items.pop();
@@ -3131,7 +3131,7 @@ suite('rendered node reuse', () => {
31313131
};
31323132
const state = model({ isMounted: false });
31333133
const jsx = <p ref={refFunc}>hello, world!</p>;
3134-
jsx.retain();
3134+
jsx.retain?.();
31353135
mount(testRoot, <div>{calc(() => state.isMounted && jsx)}</div>);
31363136

31373137
assert.deepEqual([], references);
@@ -3203,7 +3203,7 @@ suite('rendered node reuse', () => {
32033203
</p>
32043204
</div>
32053205
);
3206-
jsx.retain();
3206+
jsx.retain?.();
32073207
mount(testRoot, <div>{calc(() => state.isMounted && jsx)}</div>);
32083208

32093209
assert.deepEqual([], references);
@@ -3279,7 +3279,7 @@ suite('rendered node reuse', () => {
32793279
<strong>hello</strong>, <em>world</em>!
32803280
</span>
32813281
);
3282-
jsx.retain();
3282+
jsx.retain?.();
32833283
mount(
32843284
testRoot,
32853285
<div>
@@ -3324,7 +3324,7 @@ suite('rendered node reuse', () => {
33243324
<strong>hello</strong>, <em>world</em>!
33253325
</span>
33263326
);
3327-
jsx.retain();
3327+
jsx.retain?.();
33283328
const leftMount = document.createElement('div');
33293329
const rightMount = document.createElement('div');
33303330

@@ -3375,7 +3375,7 @@ suite('rendered node reuse', () => {
33753375
<strong>hello</strong>, <em>world</em>!
33763376
</button>
33773377
);
3378-
jsx.retain();
3378+
jsx.retain?.();
33793379
const leftMount = document.createElement('div');
33803380
const rightMount = document.createElement('div');
33813381

@@ -5501,7 +5501,7 @@ suite('custom elements', () => {
55015501
<div id="f">f</div>,
55025502
];
55035503
for (const rn of jsx) {
5504-
rn.retain();
5504+
rn.retain?.();
55055505
}
55065506

55075507
const indexOffset = field(0);
@@ -5586,7 +5586,7 @@ suite('custom elements', () => {
55865586

55875587
unmount();
55885588
for (const rn of jsx) {
5589-
rn.release();
5589+
rn.release?.();
55905590
}
55915591
});
55925592

0 commit comments

Comments
 (0)