Skip to content

Commit 38ef431

Browse files
committed
feat: working toJSON method
1 parent 5186552 commit 38ef431

File tree

3 files changed

+202
-9
lines changed

3 files changed

+202
-9
lines changed

src/renderer/reconciler.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,24 @@ import { DefaultEventPriority } from 'react-reconciler/constants';
33

44
export type Type = string;
55
export type Props = object;
6-
export type HostContext = object;
76
export type OpaqueHandle = Fiber;
87
export type PublicInstance = unknown | TextInstance;
98
export type SuspenseInstance = unknown;
109
export type UpdatePayload = unknown;
1110

1211
export type Container = {
1312
tag: 'CONTAINER';
14-
children: Array<Instance | TextInstance | SuspenseInstance>; // Added SuspenseInstance
13+
children: Array<Instance | TextInstance>; // Added SuspenseInstance
1514
createNodeMock: Function;
1615
};
1716

1817
export type Instance = {
1918
tag: 'INSTANCE';
2019
type: string;
2120
props: object;
22-
isHidden: boolean;
23-
children: Array<Instance | TextInstance | SuspenseInstance>;
21+
children: Array<Instance | TextInstance>;
2422
rootContainer: Container;
23+
isHidden: boolean;
2524
internalHandle: OpaqueHandle;
2625
};
2726

@@ -31,6 +30,10 @@ export type TextInstance = {
3130
isHidden: boolean;
3231
};
3332

33+
type HostContext = {
34+
isInsideText: boolean;
35+
};
36+
3437
const NO_CONTEXT = {};
3538
const UPDATE_SIGNAL = {};
3639
const nodeToInstanceMap = new WeakMap<object, Instance>();
@@ -98,6 +101,10 @@ const hostConfig = {
98101
_hostContext: HostContext,
99102
internalHandle: OpaqueHandle,
100103
): Instance {
104+
console.log('createInstance', type, props);
105+
console.log('- RootContainer:', rootContainer);
106+
console.log('- HostContext:', _hostContext);
107+
console.log('- InternalHandle:', internalHandle);
101108
return {
102109
tag: 'INSTANCE',
103110
type,
@@ -115,9 +122,13 @@ const hostConfig = {
115122
createTextInstance(
116123
text: string,
117124
_rootContainer: Container,
118-
_hostContext: HostContext,
125+
hostContext: HostContext,
119126
_internalHandle: OpaqueHandle,
120127
): TextInstance {
128+
if (!hostContext.isInsideText) {
129+
throw new Error(`Text string "${text}" must be rendered inside <Text> component`);
130+
}
131+
121132
return {
122133
tag: 'TEXT',
123134
text,
@@ -193,7 +204,7 @@ const hostConfig = {
193204
* This method happens **in the render phase**. Do not mutate the tree from it.
194205
*/
195206
getRootHostContext(_rootContainer: Container): HostContext | null {
196-
return NO_CONTEXT;
207+
return { isInsideText: false };
197208
},
198209

199210
/**
@@ -206,11 +217,18 @@ const hostConfig = {
206217
* This method happens **in the render phase**. Do not mutate the tree from it.
207218
*/
208219
getChildHostContext(
209-
_parentHostContext: HostContext,
210-
_type: Type,
220+
parentHostContext: HostContext,
221+
type: Type,
211222
_rootContainer: Container,
212223
): HostContext {
213-
return NO_CONTEXT;
224+
const previousIsInsideText = parentHostContext.isInsideText;
225+
const isInsideText = type === 'Text';
226+
227+
if (previousIsInsideText === isInsideText) {
228+
return parentHostContext;
229+
}
230+
231+
return { isInsideText };
214232
},
215233

216234
/**

src/renderer/renderer.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as React from 'react';
2+
import { View, Text } from 'react-native';
3+
import { render } from './renderer';
4+
5+
test('renders View', () => {
6+
render(<View />);
7+
expect(true).toBe(true);
8+
});
9+
10+
test('renders Text', () => {
11+
render(<Text>Hello world</Text>);
12+
expect(true).toBe(true);
13+
});
14+
15+
test('throws when rendering string inside View', () => {
16+
expect(() => render(<View>Hello</View>)).toThrowErrorMatchingInlineSnapshot(
17+
`"Text string "Hello" must be rendered inside <Text> component"`,
18+
);
19+
});
20+
21+
test('implements toJSON()', () => {
22+
const result = render(
23+
<View testID="view">
24+
<Text style={{ color: 'blue' }}>Hello</Text>
25+
</View>,
26+
);
27+
expect(result.toJSON()).toMatchInlineSnapshot(`
28+
<View
29+
testID="view"
30+
>
31+
<Text
32+
style={
33+
{
34+
"color": "blue",
35+
}
36+
}
37+
>
38+
Hello
39+
</Text>
40+
</View>
41+
`);
42+
});

src/renderer/renderer.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { ReactElement } from 'react';
2+
import { Container, Instance, TestReconciler, TextInstance } from './reconciler';
3+
4+
export function render(element: ReactElement) {
5+
const container: Container = {
6+
tag: 'CONTAINER',
7+
children: [],
8+
createNodeMock: () => null,
9+
};
10+
11+
const root = TestReconciler.createContainer(
12+
container,
13+
0, // 0 = LegacyRoot, 1 = ConcurrentRoot
14+
null, // no hydration callback
15+
false, // isStrictMode
16+
null, // concurrentUpdatesByDefaultOverride
17+
'id', // identifierPrefix
18+
(error) => {
19+
// eslint-disable-next-line no-console
20+
console.log('Recoverable Error', error);
21+
}, // onRecoverableError
22+
null, // transitionCallbacks
23+
);
24+
25+
TestReconciler.updateContainer(element, root, null, () => {
26+
// eslint-disable-next-line no-console
27+
console.log('Rendered', container.children);
28+
});
29+
30+
const toJSON = () => {
31+
if (root?.current == null || container == null) {
32+
return null;
33+
}
34+
35+
if (container.children.length === 0) {
36+
return null;
37+
}
38+
39+
if (container.children.length === 1) {
40+
return toJson(container.children[0]);
41+
}
42+
43+
if (
44+
container.children.length === 2 &&
45+
container.children[0].isHidden === true &&
46+
container.children[1].isHidden === false
47+
) {
48+
// Omit timed out children from output entirely, including the fact that we
49+
// temporarily wrap fallback and timed out children in an array.
50+
return toJson(container.children[1]);
51+
}
52+
53+
let renderedChildren = null;
54+
if (container.children?.length) {
55+
for (let i = 0; i < container.children.length; i++) {
56+
const renderedChild = toJson(container.children[i]);
57+
if (renderedChild !== null) {
58+
if (renderedChildren === null) {
59+
renderedChildren = [renderedChild];
60+
} else {
61+
renderedChildren.push(renderedChild);
62+
}
63+
}
64+
}
65+
}
66+
67+
return renderedChildren;
68+
};
69+
70+
return {
71+
toJSON,
72+
};
73+
}
74+
75+
type ToJsonNode = ToJsonInstance | string;
76+
77+
type ToJsonInstance = {
78+
type: string;
79+
props: object;
80+
children: Array<ToJsonNode> | null;
81+
$$typeof: Symbol;
82+
};
83+
84+
function toJson(instance: Instance | TextInstance): ToJsonNode | null {
85+
if (instance.isHidden) {
86+
// Omit timed out children from output entirely. This seems like the least
87+
// surprising behavior. We could perhaps add a separate API that includes
88+
// them, if it turns out people need it.
89+
return null;
90+
}
91+
92+
switch (instance.tag) {
93+
case 'TEXT':
94+
return instance.text;
95+
96+
case 'INSTANCE': {
97+
// We don't include the `children` prop in JSON.
98+
// Instead, we will include the actual rendered children.
99+
// @ts-expect-error
100+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
101+
const { children, ...props } = instance.props;
102+
103+
let renderedChildren = null;
104+
if (instance.children?.length) {
105+
for (let i = 0; i < instance.children.length; i++) {
106+
const renderedChild = toJson(instance.children[i]);
107+
if (renderedChild !== null) {
108+
if (renderedChildren === null) {
109+
renderedChildren = [renderedChild];
110+
} else {
111+
renderedChildren.push(renderedChild);
112+
}
113+
}
114+
}
115+
}
116+
117+
const result = {
118+
type: instance.type,
119+
props: props,
120+
children: renderedChildren,
121+
$$typeof: Symbol.for('react.test.json'),
122+
};
123+
Object.defineProperty(result, '$$typeof', {
124+
value: Symbol.for('react.test.json'),
125+
});
126+
return result;
127+
}
128+
129+
default:
130+
// @ts-expect-error
131+
throw new Error(`Unexpected node type in toJSON: ${inst.tag}`);
132+
}
133+
}

0 commit comments

Comments
 (0)