• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

preactjs / preact / 8317480681

17 Mar 2024 06:32PM UTC coverage: 98.935% (-0.5%) from 99.467%
8317480681

push

github

web-flow
align state updater type (#4306)

1211 of 1271 branches covered (95.28%)

743 of 751 relevant lines covered (98.93%)

784.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.95
/src/diff/index.js
1
import {
2
        EMPTY_OBJ,
3
        MODE_HYDRATE,
4
        MODE_SUSPENDED,
5
        RESET_MODE
6
} from '../constants';
7
import { BaseComponent, getDomSibling } from '../component';
8
import { Fragment } from '../create-element';
9
import { diffChildren } from './children';
10
import { setProperty } from './props';
11
import { assign, isArray, removeNode, slice } from '../util';
12
import options from '../options';
13

14
/**
15
 * Diff two virtual nodes and apply proper changes to the DOM
16
 * @param {PreactElement} parentDom The parent of the DOM element
17
 * @param {VNode} newVNode The new virtual node
18
 * @param {VNode} oldVNode The old virtual node
19
 * @param {object} globalContext The current context object. Modified by
20
 * getChildContext
21
 * @param {boolean} isSvg Whether or not this element is an SVG node
22
 * @param {Array<PreactElement>} excessDomChildren
23
 * @param {Array<Component>} commitQueue List of components which have callbacks
24
 * to invoke in commitRoot
25
 * @param {PreactElement} oldDom The current attached DOM element any new dom
26
 * elements should be placed around. Likely `null` on first render (except when
27
 * hydrating). Can be a sibling DOM element when diffing Fragments that have
28
 * siblings. In most cases, it starts out as `oldChildren[0]._dom`.
29
 * @param {boolean} isHydrating Whether or not we are in hydration
30
 * @param {any[]} refQueue an array of elements needed to invoke refs
31
 */
32
export function diff(
33
        parentDom,
34
        newVNode,
35
        oldVNode,
36
        globalContext,
37
        isSvg,
38
        excessDomChildren,
39
        commitQueue,
40
        oldDom,
41
        isHydrating,
42
        refQueue
43
) {
44
        /** @type {any} */
45
        let tmp,
46
                newType = newVNode.type;
10,827✔
47

48
        // When passing through createElement it assigns the object
49
        // constructor as undefined. This to prevent JSON-injection.
50
        if (newVNode.constructor !== undefined) return null;
10,827✔
51

52
        // If the previous diff bailed out, resume creating/hydrating.
53
        if (oldVNode._flags & MODE_SUSPENDED) {
54
                isHydrating = !!(oldVNode._flags & MODE_HYDRATE);
14✔
55
                oldDom = newVNode._dom = oldVNode._dom;
16,215✔
56
                excessDomChildren = [oldDom];
57
        }
58

59
        if ((tmp = options._diff)) tmp(newVNode);
60

61
        outer: if (typeof newType == 'function') {
10,817✔
62
                try {
5,277✔
63
                        let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
64
                        let newProps = newVNode.props;
65

66
                        // Necessary for createContext api. Setting this property will pass
67
                        // the context value as `this.context` just for this component.
68
                        tmp = newType.contextType;
69
                        let provider = tmp && globalContext[tmp._id];
5,319✔
70
                        let componentContext = tmp
5,277✔
71
                                ? provider
42✔
72
                                        ? provider.props.value
73
                                        : tmp._defaultValue
74
                                : globalContext;
75

76
                        // Get component and set it to `c`
77
                        if (oldVNode._component) {
5,277✔
78
                                c = newVNode._component = oldVNode._component;
79
                                clearProcessingException = c._processingException = c._pendingError;
80
                        } else {
81
                                // Instantiate the new component
82
                                if ('prototype' in newType && newType.prototype.render) {
9,633✔
83
                                        // @ts-expect-error The check above verifies that newType is suppose to be constructed
84
                                        newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap
85
                                } else {
86
                                        // @ts-expect-error Trust me, Component implements the interface we want
87
                                        newVNode._component = c = new BaseComponent(
88
                                                newProps,
89
                                                componentContext
90
                                        );
91
                                        c.constructor = newType;
92
                                        c.render = doRender;
93
                                }
94
                                if (provider) provider.sub(c);
3,237✔
95

96
                                c.props = newProps;
97
                                if (!c.state) c.state = {};
6,237✔
98
                                c.context = componentContext;
99
                                c._globalContext = globalContext;
100
                                isNew = c._dirty = true;
101
                                c._renderCallbacks = [];
102
                                c._stateCallbacks = [];
103
                        }
104

105
                        // Invoke getDerivedStateFromProps
106
                        if (c._nextState == null) {
8,479✔
107
                                c._nextState = c.state;
108
                        }
109

110
                        if (newType.getDerivedStateFromProps != null) {
5,314✔
111
                                if (c._nextState == c.state) {
71✔
112
                                        c._nextState = assign({}, c._nextState);
113
                                }
114

115
                                assign(
116
                                        c._nextState,
117
                                        newType.getDerivedStateFromProps(newProps, c._nextState)
118
                                );
119
                        }
120

121
                        oldProps = c.props;
122
                        oldState = c.state;
123
                        c._vnode = newVNode;
124

125
                        // Invoke pre-render lifecycle methods
126
                        if (isNew) {
5,277✔
127
                                if (
128
                                        newType.getDerivedStateFromProps == null &&
3,207✔
129
                                        c.componentWillMount != null
130
                                ) {
131
                                        c.componentWillMount();
132
                                }
133

134
                                if (c.componentDidMount != null) {
3,307✔
135
                                        c._renderCallbacks.push(c.componentDidMount);
136
                                }
137
                        } else {
138
                                if (
139
                                        newType.getDerivedStateFromProps == null &&
5,347✔
140
                                        newProps !== oldProps &&
141
                                        c.componentWillReceiveProps != null
142
                                ) {
143
                                        c.componentWillReceiveProps(newProps, componentContext);
5,807✔
144
                                }
145

146
                                if (
2,066✔
147
                                        !c._force &&
148
                                        ((c.shouldComponentUpdate != null &&
149
                                                c.shouldComponentUpdate(
150
                                                        newProps,
151
                                                        c._nextState,
152
                                                        componentContext
153
                                                ) === false) ||
154
                                                newVNode._original === oldVNode._original)
155
                                ) {
156
                                        // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
157
                                        if (newVNode._original !== oldVNode._original) {
83✔
158
                                                // When we are dealing with a bail because of sCU we have to update
159
                                                // the props, state and dirty-state.
160
                                                // when we are dealing with strict-equality we don't as the child could still
161
                                                // be dirtied see #3883
162
                                                c.props = newProps;
163
                                                c.state = c._nextState;
164
                                                c._dirty = false;
165
                                        }
166

167
                                        newVNode._dom = oldVNode._dom;
168
                                        newVNode._children = oldVNode._children;
169
                                        newVNode._children.forEach(vnode => {
170
                                                if (vnode) vnode._parent = newVNode;
53✔
171
                                        });
172

173
                                        for (let i = 0; i < c._stateCallbacks.length; i++) {
48✔
174
                                                c._renderCallbacks.push(c._stateCallbacks[i]);
×
175
                                        }
176
                                        c._stateCallbacks = [];
48✔
177

178
                                        if (c._renderCallbacks.length) {
48!
179
                                                commitQueue.push(c);
180
                                        }
181

182
                                        break outer;
48✔
183
                                }
184

185
                                if (c.componentWillUpdate != null) {
2,014✔
186
                                        c.componentWillUpdate(newProps, c._nextState, componentContext);
187
                                }
188

189
                                if (c.componentDidUpdate != null) {
2,071✔
190
                                        c._renderCallbacks.push(() => {
191
                                                c.componentDidUpdate(oldProps, oldState, snapshot);
59✔
192
                                        });
193
                                }
194
                        }
195

196
                        c.context = componentContext;
197
                        c.props = newProps;
198
                        c._parentDom = parentDom;
199
                        c._force = false;
200

201
                        let renderHook = options._render,
202
                                count = 0;
203
                        if ('prototype' in newType && newType.prototype.render) {
5,214✔
204
                                c.state = c._nextState;
205
                                c._dirty = false;
206

207
                                if (renderHook) renderHook(newVNode);
2,136✔
208

209
                                tmp = c.render(c.props, c.state, c.context);
210

211
                                for (let i = 0; i < c._stateCallbacks.length; i++) {
1,446✔
212
                                        c._renderCallbacks.push(c._stateCallbacks[i]);
13✔
213
                                }
214
                                c._stateCallbacks = [];
1,394✔
215
                        } else {
216
                                do {
3,768✔
217
                                        c._dirty = false;
3,787✔
218
                                        if (renderHook) renderHook(newVNode);
6,216✔
219

220
                                        tmp = c.render(c.props, c.state, c.context);
221

222
                                        // Handle setState called in render, see #2553
223
                                        c.state = c._nextState;
224
                                } while (c._dirty && ++count < 25);
3,687✔
225
                        }
226

227
                        // Handle setState called in render, see #2553
228
                        c.state = c._nextState;
5,043✔
229

230
                        if (c.getChildContext != null) {
5,189✔
231
                                globalContext = assign(assign({}, globalContext), c.getChildContext());
232
                        }
233

234
                        if (!isNew && c.getSnapshotBeforeUpdate != null) {
7,056✔
235
                                snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
236
                        }
237

238
                        let isTopLevelFragment =
239
                                tmp != null && tmp.type === Fragment && tmp.key == null;
14,954✔
240
                        let renderResult = isTopLevelFragment ? tmp.props.children : tmp;
241

242
                        diffChildren(
243
                                parentDom,
244
                                isArray(renderResult) ? renderResult : [renderResult],
5,041✔
245
                                newVNode,
246
                                oldVNode,
247
                                globalContext,
248
                                isSvg,
249
                                excessDomChildren,
250
                                commitQueue,
251
                                oldDom,
252
                                isHydrating,
253
                                refQueue
254
                        );
255

256
                        c.base = newVNode._dom;
257

258
                        // We successfully rendered this VNode, unset any stored hydration/bailout state:
259
                        newVNode._flags &= RESET_MODE;
260

261
                        if (c._renderCallbacks.length) {
5,308✔
262
                                commitQueue.push(c);
263
                        }
264

265
                        if (clearProcessingException) {
5,078✔
266
                                c._pendingError = c._processingException = null;
267
                        }
268
                } catch (e) {
269
                        newVNode._original = null;
219✔
270
                        // if hydrating or creating initial tree, bailout preserves DOM:
271
                        if (isHydrating || excessDomChildren != null) {
641✔
272
                                newVNode._dom = oldDom;
273
                                newVNode._flags |= isHydrating
274
                                        ? MODE_HYDRATE | MODE_SUSPENDED
275
                                        : MODE_HYDRATE;
276
                                excessDomChildren[excessDomChildren.indexOf(oldDom)] = null;
277
                                // ^ could possibly be simplified to:
278
                                // excessDomChildren.length = 0;
279
                        } else {
280
                                newVNode._dom = oldVNode._dom;
281
                                newVNode._children = oldVNode._children;
282
                        }
283
                        options._catchError(e, newVNode, oldVNode);
284
                }
285
        } else if (
286
                excessDomChildren == null &&
5,540✔
287
                newVNode._original === oldVNode._original
288
        ) {
289
                newVNode._children = oldVNode._children;
290
                newVNode._dom = oldVNode._dom;
291
        } else {
292
                newVNode._dom = diffElementNodes(
293
                        oldVNode._dom,
294
                        newVNode,
295
                        oldVNode,
296
                        globalContext,
297
                        isSvg,
298
                        excessDomChildren,
299
                        commitQueue,
300
                        isHydrating,
301
                        refQueue
10,770✔
302
                );
303
        }
304

305
        if ((tmp = options.diffed)) tmp(newVNode);
306
}
307

308
/**
309
 * @param {Array<Component>} commitQueue List of components
310
 * which have callbacks to invoke in commitRoot
311
 * @param {VNode} root
312
 */
313
export function commitRoot(commitQueue, root, refQueue) {
314
        root._nextDom = undefined;
1,810✔
315

316
        for (let i = 0; i < refQueue.length; i++) {
1,810✔
317
                applyRef(refQueue[i], refQueue[++i], refQueue[++i]);
90✔
318
        }
319

320
        if (options._commit) options._commit(root, commitQueue);
1,810✔
321

322
        commitQueue.some(c => {
323
                try {
294✔
324
                        // @ts-expect-error Reuse the commitQueue variable here so the type changes
325
                        commitQueue = c._renderCallbacks;
294✔
326
                        c._renderCallbacks = [];
327
                        commitQueue.some(cb => {
328
                                // @ts-expect-error See above comment on commitQueue
329
                                cb.call(c);
179✔
330
                        });
331
                } catch (e) {
332
                        options._catchError(e, c._vnode);
4✔
333
                }
334
        });
335
}
336

337
/**
338
 * Diff two virtual nodes representing DOM element
339
 * @param {PreactElement} dom The DOM element representing the virtual nodes
340
 * being diffed
341
 * @param {VNode} newVNode The new virtual node
342
 * @param {VNode} oldVNode The old virtual node
343
 * @param {object} globalContext The current context object
344
 * @param {boolean} isSvg Whether or not this DOM node is an SVG node
345
 * @param {Array<PreactElement>} excessDomChildren
346
 * @param {Array<Component>} commitQueue List of components which have callbacks
347
 * to invoke in commitRoot
348
 * @param {boolean} isHydrating Whether or not we are in hydration
349
 * @param {any[]} refQueue an array of elements needed to invoke refs
350
 * @returns {PreactElement}
351
 */
352
function diffElementNodes(
353
        dom,
354
        newVNode,
355
        oldVNode,
356
        globalContext,
357
        isSvg,
358
        excessDomChildren,
359
        commitQueue,
360
        isHydrating,
361
        refQueue
362
) {
363
        let oldProps = oldVNode.props;
5,531✔
364
        let newProps = newVNode.props;
5,531✔
365
        let nodeType = /** @type {string} */ (newVNode.type);
5,531✔
366
        /** @type {any} */
367
        let i;
368
        /** @type {{ __html?: string }} */
369
        let newHtml;
370
        /** @type {{ __html?: string }} */
371
        let oldHtml;
372
        /** @type {ComponentChildren} */
373
        let newChildren;
374
        let value;
375
        let inputValue;
376
        let checked;
377

378
        // Tracks entering and exiting SVG namespace when descending through the tree.
379
        if (nodeType === 'svg') isSvg = true;
5,534✔
380

381
        if (excessDomChildren != null) {
5,531✔
382
                for (i = 0; i < excessDomChildren.length; i++) {
229✔
383
                        value = excessDomChildren[i];
384

385
                        // if newVNode matches an element in excessDomChildren or the `dom`
386
                        // argument matches an element in excessDomChildren, remove it from
387
                        // excessDomChildren so it isn't later removed in diffChildren
388
                        if (
362✔
389
                                value &&
390
                                'setAttribute' in value === !!nodeType &&
391
                                (nodeType ? value.localName === nodeType : value.nodeType === 3)
199✔
392
                        ) {
393
                                dom = value;
195✔
394
                                excessDomChildren[i] = null;
395
                                break;
195✔
396
                        }
397
                }
398
        }
399

400
        if (dom == null) {
5,531✔
401
                if (nodeType === null) {
3,562✔
402
                        return document.createTextNode(newProps);
1,694✔
403
                }
404

405
                if (isSvg) {
1,868✔
406
                        dom = document.createElementNS('http://www.w3.org/2000/svg', nodeType);
1,868✔
407
                } else {
408
                        dom = document.createElement(nodeType, newProps.is && newProps);
1,863✔
409
                }
410

411
                // we created a new parent, so none of the previously attached children can be reused:
412
                excessDomChildren = null;
413
                // we are creating a new node, so we can assume this is a new subtree (in
414
                // case we are hydrating), this deopts the hydrate
415
                isHydrating = false;
416
        }
417

418
        if (nodeType === null) {
3,837✔
419
                // During hydration, we still have to split merged text from SSR'd HTML.
420
                if (oldProps !== newProps && (!isHydrating || dom.data !== newProps)) {
887✔
421
                        dom.data = newProps;
422
                }
423
        } else {
424
                // If excessDomChildren was not null, repopulate it with the current element's children:
425
                excessDomChildren = excessDomChildren && slice.call(dom.childNodes);
3,058✔
426

427
                oldProps = oldVNode.props || EMPTY_OBJ;
10,718✔
428

429
                // If we are in a situation where we are not hydrating but are using
430
                // existing DOM (e.g. replaceNode) we should read the existing DOM
431
                // attributes to diff them
432
                if (!isHydrating && excessDomChildren != null) {
2,950!
433
                        oldProps = {};
434
                        for (i = 0; i < dom.attributes.length; i++) {
×
435
                                value = dom.attributes[i];
436
                                oldProps[value.name] = value.value;
×
437
                        }
438
                }
439

440
                for (i in oldProps) {
2,950✔
441
                        value = oldProps[i];
1,041✔
442
                        if (i == 'children') {
1,207✔
443
                        } else if (i == 'dangerouslySetInnerHTML') {
166!
444
                                oldHtml = value;
445
                        } else if (i !== 'key' && !(i in newProps)) {
343✔
446
                                setProperty(dom, i, null, value, isSvg);
447
                        }
448
                }
449

450
                // During hydration, props are not diffed at all (including dangerouslySetInnerHTML)
451
                // @TODO we should warn in debug mode when props don't match here.
452
                for (i in newProps) {
2,950✔
453
                        value = newProps[i];
3,176✔
454
                        if (i == 'children') {
3,176✔
455
                                newChildren = value;
456
                        } else if (i == 'dangerouslySetInnerHTML') {
518✔
457
                                newHtml = value;
458
                        } else if (i == 'value') {
517✔
459
                                inputValue = value;
460
                        } else if (i == 'checked') {
487✔
461
                                checked = value;
462
                        } else if (
463
                                i !== 'key' &&
1,835✔
464
                                (!isHydrating || typeof value == 'function') &&
465
                                oldProps[i] !== value
466
                        ) {
467
                                setProperty(dom, i, value, oldProps[i], isSvg);
468
                        }
469
                }
470

471
                // If the new vnode didn't have dangerouslySetInnerHTML, diff its children
472
                if (newHtml) {
2,950✔
473
                        // Avoid re-applying the same '__html' if it did not changed between re-render
474
                        if (
475
                                !isHydrating &&
1!
476
                                (!oldHtml ||
477
                                        (newHtml.__html !== oldHtml.__html &&
478
                                                newHtml.__html !== dom.innerHTML))
479
                        ) {
480
                                dom.innerHTML = newHtml.__html;
481
                        }
482

483
                        newVNode._children = [];
484
                } else {
485
                        if (oldHtml) dom.innerHTML = '';
2,949!
486

487
                        diffChildren(
488
                                dom,
489
                                isArray(newChildren) ? newChildren : [newChildren],
2,949✔
490
                                newVNode,
491
                                oldVNode,
492
                                globalContext,
493
                                isSvg && nodeType !== 'foreignObject',
2,955✔
494
                                excessDomChildren,
495
                                commitQueue,
496
                                excessDomChildren
2,949✔
497
                                        ? excessDomChildren[0]
498
                                        : oldVNode._children && getDomSibling(oldVNode, 0),
3,816✔
499
                                isHydrating,
500
                                refQueue
501
                        );
502

503
                        // Remove children that are not part of any vnode.
504
                        if (excessDomChildren != null) {
2,949✔
505
                                for (i = excessDomChildren.length; i--; ) {
107✔
506
                                        if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]);
151✔
507
                                }
508
                        }
509
                }
510

511
                // As above, don't diff props during hydration
512
                if (!isHydrating) {
2,950✔
513
                        i = 'value';
2,901!
514
                        if (
515
                                inputValue !== undefined &&
516
                                // #2756 For the <progress>-element the initial value is 0,
517
                                // despite the attribute not being present. When the attribute
518
                                // is missing the progress bar is treated as indeterminate.
519
                                // To fix that we'll always update it when it is 0 for progress elements
520
                                (inputValue !== dom[i] ||
521
                                        (nodeType === 'progress' && !inputValue) ||
522
                                        // This is only for IE 11 to fix <select> value not being updated.
523
                                        // To avoid a stale select value we need to set the option.value
524
                                        // again, which triggers IE11 to re-evaluate the select value
525
                                        (nodeType === 'option' && inputValue !== oldProps[i]))
526
                        ) {
527
                                setProperty(dom, i, inputValue, oldProps[i], false);
528
                        }
529

530
                        i = 'checked';
2,847!
531
                        if (checked !== undefined && checked !== dom[i]) {
532
                                setProperty(dom, i, checked, oldProps[i], false);
533
                        }
534
                }
535
        }
536

537
        return dom;
3,837✔
538
}
539

540
/**
541
 * Invoke or update a ref, depending on whether it is a function or object ref.
542
 * @param {Ref<any>} ref
543
 * @param {any} value
544
 * @param {VNode} vnode
545
 */
546
export function applyRef(ref, value, vnode) {
547
        try {
128✔
548
                if (typeof ref == 'function') ref(value);
128✔
549
                else ref.current = value;
550
        } catch (e) {
551
                options._catchError(e, vnode);
8✔
552
        }
553
}
554

555
/**
556
 * Unmount a virtual node from the tree and apply DOM changes
557
 * @param {VNode} vnode The virtual node to unmount
558
 * @param {VNode} parentVNode The parent of the VNode that initiated the unmount
559
 * @param {boolean} [skipRemove] Flag that indicates that a parent node of the
560
 * current element is already detached from the DOM.
561
 */
562
export function unmount(vnode, parentVNode, skipRemove) {
563
        let r;
564
        if (options.unmount) options.unmount(vnode);
4,394✔
565

566
        if ((r = vnode.ref)) {
567
                if (!r.current || r.current === vnode._dom) {
568
                        applyRef(r, null, parentVNode);
569
                }
570
        }
571

572
        if ((r = vnode._component) != null) {
1,741✔
573
                if (r.componentWillUnmount) {
495✔
574
                        try {
59✔
575
                                r.componentWillUnmount();
59✔
576
                        } catch (e) {
577
                                options._catchError(e, parentVNode);
2✔
578
                        }
579
                }
580

581
                r.base = r._parentDom = null;
495✔
582
                vnode._component = undefined;
583
        }
584

585
        if ((r = vnode._children)) {
1,741✔
586
                for (let i = 0; i < r.length; i++) {
1,073✔
587
                        if (r[i]) {
1,135✔
588
                                unmount(
589
                                        r[i],
590
                                        parentVNode,
591
                                        skipRemove || typeof vnode.type !== 'function'
2,079✔
592
                                );
593
                        }
594
                }
595
        }
596

597
        if (!skipRemove && vnode._dom != null) {
1,741✔
598
                removeNode(vnode._dom);
599
        }
600

601
        // Must be set to `undefined` to properly clean up `_nextDom`
602
        // for which `null` is a valid value. See comment in `create-element.js`
603
        vnode._parent = vnode._dom = vnode._nextDom = undefined;
604
}
605

606
/** The `.render()` method for a PFC backing instance. */
607
function doRender(props, state, context) {
608
        return this.constructor(props, context);
3,786✔
609
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc