Skip to content

Commit 02041a8

Browse files
committed
core logic for diffing children
1 parent c119b67 commit 02041a8

File tree

3 files changed

+307
-9
lines changed

3 files changed

+307
-9
lines changed
Lines changed: 240 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,242 @@
1-
describe.skip('diffNodes - Children cases', () => {
2-
test('dummy test', () => {
3-
expect(true).toBe(true)
1+
import { diffNodes } from '../diff'
2+
import { Props, RegularComponentVNode, StaticVNode, VNode } from '../types'
3+
4+
describe('diffNodes - Children cases', () => {
5+
test('returns INSERT patch when a new static child is added', () => {
6+
const oldNode: StaticVNode = {
7+
kind: 'static',
8+
type: 'div',
9+
props: {},
10+
children: [],
11+
}
12+
13+
const newNode: StaticVNode = {
14+
kind: 'static',
15+
type: 'div',
16+
props: {},
17+
children: [
18+
{
19+
kind: 'static',
20+
type: 'span',
21+
props: {},
22+
children: [],
23+
},
24+
],
25+
}
26+
27+
const patches = diffNodes(oldNode, newNode)
28+
29+
expect(patches).toEqual([
30+
{
31+
type: 'INSERT',
32+
node: newNode.children[0],
33+
index: 0,
34+
},
35+
])
436
})
37+
38+
test('returns INSERT patch when a new component child is added', () => {
39+
const oldNode: RegularComponentVNode = {
40+
kind: 'regular',
41+
type: () => ({
42+
kind: 'static',
43+
type: 'div',
44+
props: {},
45+
children: [],
46+
}),
47+
props: {},
48+
children: [],
49+
}
50+
51+
const newNode: RegularComponentVNode = {
52+
kind: 'regular',
53+
type: () => ({
54+
kind: 'static',
55+
type: 'div',
56+
props: {},
57+
children: [],
58+
}),
59+
props: {},
60+
children: [
61+
{
62+
kind: 'static',
63+
type: 'span',
64+
props: {},
65+
children: [],
66+
},
67+
],
68+
}
69+
70+
const patches = diffNodes(oldNode, newNode)
71+
72+
expect(patches).toEqual([
73+
{
74+
type: 'INSERT',
75+
node: newNode.children[0],
76+
index: 0,
77+
},
78+
])
79+
})
80+
81+
test('returns REMOVE patch when a static child is removed', () => {
82+
const oldNode: StaticVNode = {
83+
kind: 'static',
84+
type: 'div',
85+
props: {},
86+
children: [
87+
{
88+
kind: 'static',
89+
type: 'span',
90+
props: {},
91+
children: [],
92+
},
93+
],
94+
}
95+
96+
const newNode: StaticVNode = {
97+
kind: 'static',
98+
type: 'div',
99+
props: {},
100+
children: [],
101+
}
102+
103+
const patches = diffNodes(oldNode, newNode)
104+
105+
expect(patches).toEqual([
106+
{
107+
type: 'REMOVE',
108+
node: oldNode.children[0],
109+
},
110+
])
111+
})
112+
113+
test('returns REMOVE patch when a component child is removed', () => {
114+
const ComponentFn = (props: Props): VNode => ({
115+
kind: 'static',
116+
type: 'div',
117+
props,
118+
children: [],
119+
})
120+
121+
const oldNode: StaticVNode = {
122+
kind: 'static',
123+
type: 'div',
124+
props: {},
125+
children: [
126+
{
127+
kind: 'regular',
128+
type: ComponentFn,
129+
props: {},
130+
children: [],
131+
},
132+
],
133+
}
134+
135+
const newNode: StaticVNode = {
136+
kind: 'static',
137+
type: 'div',
138+
props: {},
139+
children: [],
140+
}
141+
142+
const patches = diffNodes(oldNode, newNode)
143+
144+
expect(patches).toEqual([
145+
{
146+
type: 'REMOVE',
147+
node: oldNode.children[0],
148+
},
149+
])
150+
})
151+
152+
// Simple child operations
153+
test('returns no patches when static children are identical', () => {
154+
const child: StaticVNode = {
155+
kind: 'static',
156+
type: 'span',
157+
props: {},
158+
children: [],
159+
}
160+
161+
const oldNode: StaticVNode = {
162+
kind: 'static',
163+
type: 'div',
164+
props: {},
165+
children: [child],
166+
}
167+
168+
const newNode: StaticVNode = {
169+
kind: 'static',
170+
type: 'div',
171+
props: {},
172+
children: [child],
173+
}
174+
175+
const patches = diffNodes(oldNode, newNode)
176+
expect(patches).toEqual([])
177+
})
178+
179+
test('returns no patches when component children are identical', () => {
180+
const ComponentFn = (props: Props): VNode => ({
181+
kind: 'static',
182+
type: 'div',
183+
props: {},
184+
children: [],
185+
})
186+
187+
const child: RegularComponentVNode = {
188+
kind: 'regular',
189+
type: ComponentFn,
190+
props: {},
191+
children: [],
192+
}
193+
194+
const oldNode: RegularComponentVNode = {
195+
kind: 'regular',
196+
type: () => ({
197+
kind: 'static',
198+
type: 'div',
199+
props: {},
200+
children: [child],
201+
}),
202+
props: {},
203+
children: [child],
204+
}
205+
206+
const newNode: RegularComponentVNode = {
207+
kind: 'regular',
208+
type: () => ({
209+
kind: 'static',
210+
type: 'div',
211+
props: {},
212+
children: [child],
213+
}),
214+
props: {},
215+
children: [child],
216+
}
217+
218+
const patches = diffNodes(oldNode, newNode)
219+
expect(patches).toEqual([])
220+
})
221+
222+
// Text and number nodes
223+
test('handles text node changes')
224+
test('handles number node changes')
225+
test('handles mixed node types (elements, text, numbers)')
226+
227+
// Nested structures
228+
test('recursively diffs nested children')
229+
test('handles deeply nested structure changes')
230+
test('correctly patches nested children props changes')
231+
232+
// Edge cases
233+
test('handles empty children array to non-empty')
234+
test('handles non-empty children array to empty')
235+
test('handles null/undefined children')
236+
test('handles children type changes (text to element)')
237+
238+
// Mixed node kinds
239+
test('handles mix of static and component children')
240+
test('handles mix of regular and memo component children')
241+
test('preserves memoization in nested children')
5242
})

src/lib/diff.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
import { Patch, VNode } from './types'
2+
import {
3+
isMemoComponentVNode,
4+
isRegularComponentVNode,
5+
isStaticVNode,
6+
} from './utils'
27

38
export function diffNodes(oldNode: VNode, newNode: VNode): Array<Patch> {
49
const patches: Array<Patch> = []
@@ -11,7 +16,7 @@ export function diffNodes(oldNode: VNode, newNode: VNode): Array<Patch> {
1116
})
1217
}
1318

14-
if (oldNode.kind === 'memo' && newNode.kind === 'memo') {
19+
if (isMemoComponentVNode(oldNode) && isMemoComponentVNode(newNode)) {
1520
// If props are NOT the same
1621
// Means not true
1722
// Then we update all props with newProps
@@ -26,7 +31,7 @@ export function diffNodes(oldNode: VNode, newNode: VNode): Array<Patch> {
2631
}
2732
}
2833

29-
if (oldNode.kind === 'regular' && newNode.kind === 'regular') {
34+
if (isRegularComponentVNode(oldNode) && isRegularComponentVNode(newNode)) {
3035
const oldProps = oldNode.props
3136
const newProps = newNode.props
3237

@@ -47,7 +52,7 @@ export function diffNodes(oldNode: VNode, newNode: VNode): Array<Patch> {
4752
}
4853
}
4954

50-
if (oldNode.kind === 'static' && newNode.kind === 'static') {
55+
if (isStaticVNode(oldNode) && isStaticVNode(newNode)) {
5156
if (oldNode.type !== newNode.type) {
5257
patches.push({
5358
type: 'REPLACE',
@@ -71,5 +76,53 @@ export function diffNodes(oldNode: VNode, newNode: VNode): Array<Patch> {
7176
}
7277
}
7378

79+
// Did we have children or do we have new children?
80+
// Could be either cases, but we wanna recursively diff the children if we have any
81+
if (oldNode.children.length || newNode.children.length) {
82+
const oldChildren = oldNode.children
83+
const newChildren = newNode.children
84+
85+
// maxLength so that we go over all children
86+
const maxLength = Math.max(oldChildren.length, newChildren.length)
87+
88+
for (let i = 0; i < maxLength; i++) {
89+
// Either both exists or one of them doesn't
90+
// We know because of maxLength, either of them will always exist till the end
91+
const oldChild = oldChildren[i]
92+
const newChild = newChildren[i]
93+
94+
// If old child doesn't exist, we know that newChild exists
95+
if (!oldChild) {
96+
patches.push({
97+
type: 'INSERT',
98+
node: newChild, // Already VNode which is either static or component node
99+
index: i,
100+
})
101+
102+
// No need to do any more work here
103+
// Continue to the next child
104+
continue
105+
}
106+
107+
if (!newChild) {
108+
patches.push({
109+
type: 'REMOVE',
110+
node: oldChild, // Already VNode which is either static or component node
111+
})
112+
113+
// No need to do any more work here
114+
continue
115+
}
116+
117+
// Both exist! Need to diff them
118+
// We know that both are VNode
119+
// diffNodes will return patches for the children
120+
const patch = diffNodes(oldChild, newChild)
121+
patches.push(...patch)
122+
}
123+
}
124+
125+
// For the first level, this returns patches from the root
126+
// For recursive diff, it returns patches for the lower levels up to the parents
74127
return patches
75128
}

src/lib/types.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export type StaticVNode = {
1818
kind: 'static'
1919
type: string
2020
props: Props
21-
children: Array<VNode | string | number>
21+
children: Array<VNode>
22+
key?: string | number // Add optional key
2223
}
2324

2425
// A component takes props and returns a VNode
@@ -29,7 +30,8 @@ export type RegularComponentVNode = {
2930
kind: 'regular'
3031
type: ComponentFunction
3132
props: Props
32-
children: Array<VNode | string | number>
33+
children: Array<VNode>
34+
key?: string | number // Add optional key
3335
}
3436

3537
/** Memoized component */
@@ -38,7 +40,8 @@ export type MemoComponentVNode = {
3840
type: ComponentFunction
3941
compare: (prevProps: Props, nextProps: Props) => boolean
4042
props: Props
41-
children: Array<VNode | string | number>
43+
children: Array<VNode>
44+
key?: string | number // Add optional key
4245
}
4346

4447
export type ComponentVNode = RegularComponentVNode | MemoComponentVNode
@@ -65,12 +68,17 @@ export type ReorderPatch = {
6568
toIndex: number
6669
}
6770

71+
// Here we need to know the index
72+
// Otherwise we won't know where to insert the node
6873
export type InsertPatch = {
6974
type: 'INSERT'
7075
node: VNode
7176
index: number
7277
}
7378

79+
// Here index doesn't matter
80+
// When we find the node, we know to remove it
81+
// We know it's already in the tree
7482
export type RemovePatch = {
7583
type: 'REMOVE'
7684
node: VNode

0 commit comments

Comments
 (0)