Skip to content

Commit cdc7c68

Browse files
committed
chore: implement root, update & unmount
1 parent 38ef431 commit cdc7c68

File tree

5 files changed

+245
-81
lines changed

5 files changed

+245
-81
lines changed

src/renderer/host-element.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Container, Instance, TextInstance } from './reconciler';
2+
3+
export type HostNode = HostElement | string;
4+
export type HostElementProps = Record<string, unknown>;
5+
6+
const instanceToHostElementMap = new WeakMap<Container | Instance, HostElement>();
7+
8+
export class HostElement {
9+
public type: string;
10+
public props: HostElementProps;
11+
public children: HostNode[];
12+
13+
constructor(type: string, props: HostElementProps, children: HostNode[]) {
14+
this.type = type;
15+
this.props = props;
16+
this.children = children;
17+
}
18+
19+
static fromContainer(container: Container): HostElement {
20+
const hostElement = instanceToHostElementMap.get(container);
21+
if (hostElement) {
22+
return hostElement;
23+
}
24+
25+
const result = new HostElement(
26+
'ROOT',
27+
{},
28+
container.children.map((child) => HostElement.fromInstance(child)),
29+
);
30+
31+
instanceToHostElementMap.set(container, result);
32+
return result;
33+
}
34+
35+
static fromInstance(instance: Instance | TextInstance): HostNode {
36+
switch (instance.tag) {
37+
case 'TEXT':
38+
return instance.text;
39+
40+
case 'INSTANCE': {
41+
const hostElement = instanceToHostElementMap.get(instance);
42+
if (hostElement) {
43+
return hostElement;
44+
}
45+
46+
const result = new HostElement(
47+
instance.type,
48+
instance.props,
49+
instance.children.map((child) => HostElement.fromInstance(child)),
50+
);
51+
52+
instanceToHostElementMap.set(instance, result);
53+
return result;
54+
}
55+
56+
default:
57+
// @ts-expect-error
58+
throw new Error(`Unexpected node type in toJSON: ${instance.tag}`);
59+
}
60+
}
61+
}

src/renderer/reconciler.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import createReconciler, { Fiber } from 'react-reconciler';
22
import { DefaultEventPriority } from 'react-reconciler/constants';
33

44
export type Type = string;
5-
export type Props = object;
5+
export type Props = Record<string, unknown>;
66
export type OpaqueHandle = Fiber;
77
export type PublicInstance = unknown | TextInstance;
88
export type SuspenseInstance = unknown;
@@ -17,7 +17,7 @@ export type Container = {
1717
export type Instance = {
1818
tag: 'INSTANCE';
1919
type: string;
20-
props: object;
20+
props: Props;
2121
children: Array<Instance | TextInstance>;
2222
rootContainer: Container;
2323
isHidden: boolean;
@@ -426,7 +426,7 @@ const hostConfig = {
426426
insertBefore(
427427
parentInstance: Instance,
428428
child: Instance | TextInstance,
429-
beforeChild: Instance | TextInstance | SuspenseInstance,
429+
beforeChild: Instance | TextInstance,
430430
): void {
431431
const index = parentInstance.children.indexOf(child);
432432
if (index !== -1) {
@@ -443,7 +443,7 @@ const hostConfig = {
443443
insertInContainerBefore(
444444
container: Container,
445445
child: Instance | TextInstance,
446-
beforeChild: Instance | TextInstance | SuspenseInstance,
446+
beforeChild: Instance | TextInstance,
447447
): void {
448448
const index = container.children.indexOf(child);
449449
if (index !== -1) {
@@ -459,18 +459,15 @@ const hostConfig = {
459459
*
460460
* React will only call it for the top-level node that is being removed. It is expected that garbage collection would take care of the whole subtree. You are not expected to traverse the child tree in it.
461461
*/
462-
removeChild(parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance): void {
462+
removeChild(parentInstance: Instance, child: Instance | TextInstance): void {
463463
const index = parentInstance.children.indexOf(child);
464464
parentInstance.children.splice(index, 1);
465465
},
466466

467467
/**
468468
* Same as `removeChild`, but for when a node is detached from the root container. This is useful if attaching to the root has a slightly different implementation, or if the root container nodes are of a different type than the rest of the tree.
469469
*/
470-
removeChildFromContainer(
471-
container: Container,
472-
child: Instance | TextInstance | SuspenseInstance,
473-
): void {
470+
removeChildFromContainer(container: Container, child: Instance | TextInstance): void {
474471
const index = container.children.indexOf(child);
475472
container.children.splice(index, 1);
476473
},

src/renderer/render-to-json.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Instance, TextInstance } from './reconciler';
2+
3+
export type JsonNode = JsonInstance | string;
4+
5+
export type JsonInstance = {
6+
type: string;
7+
props: object;
8+
children: Array<JsonNode> | null;
9+
$$typeof: Symbol;
10+
};
11+
12+
export function renderToJson(instance: Instance | TextInstance): JsonNode | null {
13+
if (instance.isHidden) {
14+
// Omit timed out children from output entirely. This seems like the least
15+
// surprising behavior. We could perhaps add a separate API that includes
16+
// them, if it turns out people need it.
17+
return null;
18+
}
19+
20+
switch (instance.tag) {
21+
case 'TEXT':
22+
return instance.text;
23+
24+
case 'INSTANCE': {
25+
// We don't include the `children` prop in JSON.
26+
// Instead, we will include the actual rendered children.
27+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
28+
const { children, ...props } = instance.props;
29+
30+
let renderedChildren = null;
31+
if (instance.children?.length) {
32+
for (let i = 0; i < instance.children.length; i++) {
33+
const renderedChild = renderToJson(instance.children[i]);
34+
if (renderedChild !== null) {
35+
if (renderedChildren === null) {
36+
renderedChildren = [renderedChild];
37+
} else {
38+
renderedChildren.push(renderedChild);
39+
}
40+
}
41+
}
42+
}
43+
44+
const result = {
45+
type: instance.type,
46+
props: props,
47+
children: renderedChildren,
48+
$$typeof: Symbol.for('react.test.json'),
49+
};
50+
// This is needed for JEST to format snapshot as JSX.
51+
Object.defineProperty(result, '$$typeof', {
52+
value: Symbol.for('react.test.json'),
53+
});
54+
return result;
55+
}
56+
57+
default:
58+
// @ts-expect-error
59+
throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);
60+
}
61+
}

src/renderer/renderer.test.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,62 @@ test('throws when rendering string inside View', () => {
1818
);
1919
});
2020

21+
test('implements update()', () => {
22+
const result = render(<View testID="view" />);
23+
expect(result.toJSON()).toMatchInlineSnapshot(`
24+
<View
25+
testID="view"
26+
/>
27+
`);
28+
29+
result.update(
30+
<View testID="view">
31+
<Text>Hello</Text>
32+
</View>,
33+
);
34+
expect(result.toJSON()).toMatchInlineSnapshot(`
35+
<View
36+
testID="view"
37+
>
38+
<Text>
39+
Hello
40+
</Text>
41+
</View>
42+
`);
43+
});
44+
45+
test('implements unmount()', () => {
46+
const result = render(<View testID="view" />);
47+
expect(result.toJSON()).toMatchInlineSnapshot(`
48+
<View
49+
testID="view"
50+
/>
51+
`);
52+
53+
result.unmount();
54+
expect(result.toJSON()).toBeNull();
55+
});
56+
57+
test('implements get root()', () => {
58+
const result = render(<View testID="view" />);
59+
expect(result.root).toMatchInlineSnapshot(`
60+
HostElement {
61+
"children": [
62+
HostElement {
63+
"children": [],
64+
"props": {
65+
"children": undefined,
66+
"testID": "view",
67+
},
68+
"type": "View",
69+
},
70+
],
71+
"props": {},
72+
"type": "ROOT",
73+
}
74+
`);
75+
});
76+
2177
test('implements toJSON()', () => {
2278
const result = render(
2379
<View testID="view">

0 commit comments

Comments
 (0)