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

preactjs / preact / 30626296472

31 Jul 2026 11:13AM UTC coverage: 99.292% (+0.006%) from 99.286%
30626296472

push

github

web-flow
Merge pull request #5186 from preactjs/JoviDeCroock/use-hook-compat

implement use in compat

582 of 600 branches covered (97.0%)

28 of 29 new or added lines in 2 files covered. (96.55%)

2105 of 2120 relevant lines covered (99.29%)

112.52 hits per line

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

98.81
/compat/src/render.js
1
import {
1✔
2
        render as preactRender,
3
        hydrate as preactHydrate,
1✔
4
        options,
1✔
5
        toChildArray,
1✔
6
        Component
1✔
7
} from 'preact';
1✔
8
import {
1✔
9
        useCallback,
1✔
10
        useContext,
1✔
11
        useDebugValue,
1✔
12
        useEffect,
1✔
13
        useId,
1✔
14
        useImperativeHandle,
1✔
15
        useLayoutEffect,
1✔
16
        useMemo,
1✔
17
        useReducer,
1✔
18
        useRef,
1✔
19
        useState
1✔
20
} from 'preact/hooks';
1✔
21
import { useDeferredValue, useInsertionEffect, useTransition } from './index';
1✔
22
import { assign, IS_NON_DIMENSIONAL } from './util';
23

1✔
24
export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
1✔
25

1✔
26
const MODE_HYDRATE = 1 << 5;
1✔
27

1✔
28
const CAMEL_PROPS =
1✔
29
        /^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image(!S)|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/;
1✔
30
const CAMEL_REPLACE = /[A-Z0-9]/g;
1✔
31
const IS_DOM = typeof document !== 'undefined';
1✔
32

33
/**
1✔
34
 * This is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84
1✔
35
 * on a high level this cuts out the warnings, ... and attempts a smaller implementation
1✔
36
 * @typedef {{ _value: any; _getSnapshot: () => any }} Store
1✔
37
 */
38
export function useSyncExternalStore(
1✔
39
        subscribe,
1✔
40
        getSnapshot,
1✔
41
        getServerSnapshot
1✔
42
) {
1✔
43
        const serverRendering = options._skipEffects || hydrationRoot;
1✔
44
        const value = serverRendering
61✔
45
                ? (getServerSnapshot || getSnapshot)()
61✔
46
                : getSnapshot();
61✔
47

61✔
48
        /**
49
         * @typedef {{ _instance: Store }} StoreRef
61✔
50
         * @type {[StoreRef, (store: StoreRef) => void]}
51
         */
52
        const [{ _instance }, forceUpdate] = useState({
61✔
53
                _instance: { _value: value, _getSnapshot: getSnapshot }
61✔
54
        });
61✔
55

61✔
56
        useLayoutEffect(() => {
61✔
57
                _instance._value = value;
61✔
58
                _instance._getSnapshot = getSnapshot;
61✔
59

60
                if (didSnapshotChange(_instance)) {
61✔
61
                        forceUpdate({ _instance });
61✔
62
                }
50✔
63
        }, [subscribe, value, getSnapshot]);
50✔
64

50✔
65
        useEffect(() => {
50✔
66
                if (didSnapshotChange(_instance)) {
61✔
67
                        forceUpdate({ _instance });
61✔
68
                }
69

70
                return subscribe(() => {
28✔
71
                        if (didSnapshotChange(_instance)) {
28✔
72
                                forceUpdate({ _instance });
28✔
73
                        }
3✔
74
                });
3✔
75
        }, [subscribe]);
28✔
76

28✔
77
        return value;
28✔
78
}
28✔
79

28✔
80
/** @type {(inst: Store) => boolean} */
28✔
81
function didSnapshotChange(inst) {
28✔
82
        try {
21✔
83
                return !Object.is(inst._value, inst._getSnapshot());
1✔
84
        } catch (error) {
106✔
85
                return true;
106✔
86
        }
106✔
87
}
106✔
88

106✔
89
// Input types for which onchange should not be converted to oninput.
106✔
90
const onChangeInputType = type => /fil|che|rad/.test(type);
1✔
91

14✔
92
// Some libraries like `react-virtualized` explicitly check for this.
14✔
93
Component.prototype.isReactComponent = true;
14!
94

14✔
95
// `UNSAFE_*` lifecycle hooks
14✔
96
// Preact only ever invokes the unprefixed methods.
14✔
97
// Here we provide a base "fallback" implementation that calls any defined UNSAFE_ prefixed method.
14✔
98
// - If a component defines its own `componentDidMount()` (including via defineProperty), use that.
14✔
99
// - If a component defines `UNSAFE_componentDidMount()`, `componentDidMount` is the alias getter/setter.
100
// - If anything assigns to an `UNSAFE_*` property, the assignment is forwarded to the unprefixed property.
101
// See https://github.com/preactjs/preact/issues/1941
14✔
102
[
14✔
103
        'componentWillMount',
14✔
104
        'componentWillReceiveProps',
14✔
105
        'componentWillUpdate'
14✔
106
].forEach(key => {
14✔
107
        Object.defineProperty(Component.prototype, key, {
14✔
108
                configurable: true,
99✔
109
                get() {
99✔
110
                        return this['UNSAFE_' + key];
99✔
111
                },
962✔
112
                set(v) {
962✔
113
                        Object.defineProperty(this, key, {
99✔
114
                                configurable: true,
1✔
115
                                writable: true,
1✔
116
                                value: v
1✔
117
                        });
1✔
118
                }
1✔
119
        });
1✔
120
});
1✔
121

1✔
122
/**
1✔
123
 * Proxy render() since React returns a Component reference.
124
 * @param {import('./internal').VNode} vnode VNode tree to render
1✔
125
 * @param {import('./internal').PreactElement} parent DOM node to render vnode tree into
126
 * @param {() => void} [callback] Optional callback that will be called after rendering
1✔
127
 * @returns {import('./internal').Component | null} The root component reference or null
1✔
128
 */
1✔
129
export function render(vnode, parent, callback) {
1✔
130
        // React destroys any existing DOM nodes, see #1727
1✔
131
        // ...but only on the first render, see #1828
1✔
132
        if (parent._children == null) {
1✔
133
                parent.textContent = '';
1✔
134
        }
1✔
135

1✔
136
        preactRender(vnode, parent);
1✔
137
        if (typeof callback == 'function') callback();
1✔
138

139
        return vnode ? vnode._component : null;
1✔
140
}
1✔
141

1✔
142
export function hydrate(vnode, parent, callback) {
1✔
143
        preactHydrate(vnode, parent);
1✔
144
        if (typeof callback == 'function') callback();
1✔
145

1✔
146
        return vnode ? vnode._component : null;
1✔
147
}
1✔
148

1✔
149
let oldEventHook = options.event;
1✔
150
options.event = e => {
1✔
151
        if (oldEventHook) e = oldEventHook(e);
1✔
152

1✔
153
        e.persist = () => {};
1✔
154
        e.isPropagationStopped = function isPropagationStopped() {
45✔
155
                return this.cancelBubble;
45✔
156
        };
3✔
157
        e.isDefaultPrevented = function isDefaultPrevented() {
3✔
158
                return this.defaultPrevented;
45✔
159
        };
3✔
160
        return (e.nativeEvent = e);
3✔
161
};
3✔
162

3✔
163
const classNameDescriptorNonEnumberable = {
3✔
164
        configurable: true,
45✔
165
        get() {
1✔
166
                return this.class;
1✔
167
        }
1✔
168
};
169

170
function handleDomVNode(vnode) {
1✔
171
        let props = vnode.props,
1✔
172
                type = vnode.type,
1✔
173
                normalizedProps = {},
675✔
174
                isNonDashedType = type.indexOf('-') == -1;
675✔
175

675✔
176
        for (let i in props) {
675✔
177
                let value = props[i];
675✔
178

675✔
179
                if (
675✔
180
                        (i === 'value' && 'defaultValue' in props && value == null) ||
675✔
181
                        // Emulate React's behavior of not rendering the contents of noscript tags on the client.
801✔
182
                        (IS_DOM && i === 'children' && type === 'noscript') ||
801✔
183
                        i === 'class' ||
799✔
184
                        i === 'className'
799✔
185
                ) {
799✔
186
                        // Skip applying value if it is null/undefined and we already set
799✔
187
                        // a default value
188
                        continue;
189
                }
799✔
190

191
                if (i === 'style' && typeof value === 'object') {
801✔
192
                        let cloned;
768✔
193
                        for (let key in value) {
2✔
194
                                if (typeof value[key] === 'number' && !IS_NON_DIMENSIONAL.test(key)) {
768✔
195
                                        if (!cloned) {
157✔
196
                                                cloned = value = assign({}, value);
157✔
197
                                        }
198
                                        value[key] += 'px';
157✔
199
                                }
157✔
200
                        }
157✔
201
                } else if (
157✔
202
                        i === 'defaultValue' &&
2✔
203
                        'value' in props &&
768✔
204
                        props.value == null
766✔
205
                ) {
206
                        // `defaultValue` is treated as a fallback `value` when a value prop is present but null/undefined.
766✔
207
                        // `defaultValue` for Elements with no value prop is the same as the DOM defaultValue property.
766✔
208
                        i = 'value';
766✔
209
                } else if (i === 'download' && value === true) {
766✔
210
                        // Calling `setAttribute` with a truthy value will lead to it being
2✔
211
                        // passed as a stringified value, e.g. `download="true"`. React
2✔
212
                        // converts it to an empty string instead, otherwise the attribute
2✔
213
                        // value will be used as the file name and the file will be called
2✔
214
                        // "true" upon downloading it.
2✔
215
                        value = '';
766✔
216
                } else if (i === 'translate' && value === 'no') {
764✔
217
                        value = false;
763✔
218
                } else if (i[0] === 'o' && i[1] === 'n') {
763✔
219
                        let lowerCased = i.toLowerCase();
763✔
220
                        if (lowerCased === 'ondoubleclick') {
86✔
221
                                i = 'ondblclick';
84✔
222
                        } else if (
84✔
223
                                lowerCased === 'onchange' &&
84✔
224
                                (type === 'input' || type === 'textarea') &&
84✔
225
                                !onChangeInputType(props.type)
83✔
226
                        ) {
15✔
227
                                lowerCased = i = 'oninput';
15✔
228
                        } else if (lowerCased === 'onfocus') {
1✔
229
                                i = 'onfocusin';
1✔
230
                        } else if (lowerCased === 'onblur') {
1✔
231
                                i = 'onfocusout';
1✔
232
                        }
1✔
233

1✔
234
                        // Add support for onInput and onChange, see #3561
1✔
235
                        // if we have an oninput prop already change it to oninputCapture
1✔
236
                        if (lowerCased === 'oninput') {
1✔
237
                                i = lowerCased;
1✔
238
                                if (normalizedProps[i]) {
11✔
239
                                        i = 'oninputCapture';
11✔
240
                                }
84✔
241
                        }
84✔
242
                } else if (isNonDashedType && CAMEL_PROPS.test(i)) {
84✔
243
                        i = i.replace(CAMEL_REPLACE, '-$&').toLowerCase();
762✔
244
                } else if (value === null) {
19✔
245
                        value = undefined;
19✔
246
                }
19✔
247

19✔
248
                normalizedProps[i] = value;
19✔
249
        }
19✔
250

19✔
251
        if (type == 'select') {
19✔
252
                // Add support for array select values: <select multiple value={[]} />
5✔
253
                if (normalizedProps.multiple && Array.isArray(normalizedProps.value)) {
675✔
254
                        // forEach() always returns undefined, which we abuse here to unset the value prop.
255
                        normalizedProps.value = toChildArray(props.children).forEach(child => {
3✔
256
                                child.props.selected =
1✔
257
                                        normalizedProps.value.indexOf(child.props.value) != -1;
1✔
258
                        });
3✔
259
                }
260

261
                // Adding support for defaultValue in select tag
262
                if (normalizedProps.defaultValue != null) {
3✔
263
                        normalizedProps.value = toChildArray(props.children).forEach(child => {
3✔
264
                                if (normalizedProps.multiple) {
2✔
265
                                        child.props.selected =
6✔
266
                                                normalizedProps.defaultValue.indexOf(child.props.value) != -1;
6✔
267
                                } else {
268
                                        child.props.selected =
269
                                                normalizedProps.defaultValue == child.props.value;
3✔
270
                                }
271
                        });
3✔
272
                }
273
        }
274

275
        if (props.class && !props.className) {
3✔
276
                normalizedProps.class = props.class;
675✔
277
                Object.defineProperty(
675✔
278
                        normalizedProps,
8✔
279
                        'className',
8✔
280
                        classNameDescriptorNonEnumberable
8✔
281
                );
282
        } else if (props.className) {
8✔
283
                normalizedProps.class = normalizedProps.className = props.className;
674✔
284
        }
285

286
        vnode.props = normalizedProps;
11✔
287
}
11✔
288

289
let oldVNodeHook = options.vnode;
11✔
290
options.vnode = vnode => {
11✔
291
        // only normalize props on Element nodes
292
        if (typeof vnode.type === 'string') {
11✔
293
                handleDomVNode(vnode);
11✔
294
        } else if (typeof vnode.type === 'function') {
11✔
295
                const shouldApplyRef =
296
                        'prototype' in vnode.type && vnode.type.prototype.render;
2,598✔
297
                if ('ref' in vnode.props && shouldApplyRef) {
67✔
298
                        vnode.ref = vnode.props.ref;
1,039✔
299
                        delete vnode.props.ref;
1,110✔
300
                }
301

302
                if (vnode.type.defaultProps) {
3✔
303
                        let normalizedProps = assign({}, vnode.props);
1,972✔
304
                        for (let i in vnode.type.defaultProps) {
2,598✔
305
                                if (normalizedProps[i] === undefined) {
8✔
306
                                        normalizedProps[i] = vnode.type.defaultProps[i];
8✔
307
                                }
308
                        }
309
                        vnode.props = normalizedProps;
13✔
310
                }
6✔
311
        }
312
        vnode.$$typeof = REACT_ELEMENT_TYPE;
6✔
313

314
        if (oldVNodeHook) oldVNodeHook(vnode);
6✔
315
};
6✔
316

317
// Only needed for react-relay
318
let currentComponent, hydrationRoot;
8✔
319
const oldBeforeRender = options._render;
8✔
320
options._render = function (vnode) {
3,273✔
321
        if (oldBeforeRender) {
3,273✔
322
                oldBeforeRender(vnode);
1✔
323
        }
324
        if (vnode._flags & MODE_HYDRATE) hydrationRoot = vnode;
1✔
325
        currentComponent = vnode._component;
1,948✔
326
};
1,948✔
327

328
const oldDiffed = options.diffed;
1,948✔
329
/** @type {(vnode: import('./internal').VNode) => void} */
330
options.diffed = function (vnode) {
1✔
331
        if (oldDiffed) {
1✔
332
                oldDiffed(vnode);
1✔
333
        }
334

335
        const props = vnode.props;
1✔
336
        const dom = vnode._dom;
3,207✔
337

338
        if (
339
                dom != null &&
3,207✔
340
                vnode.type === 'textarea' &&
3,207✔
341
                'value' in props &&
3,207✔
342
                props.value !== dom.value
2,842✔
343
        ) {
344
                dom.value = props.value == null ? '' : props.value;
1,407!
345
        }
346

347
        currentComponent = null;
×
348
        if (hydrationRoot == vnode) hydrationRoot = null;
×
349
};
×
350

351
/**
352
 * Read the value of a Promise (suspending while pending) or a Context.
353
 * Unlike other hooks, `use` may be called conditionally.
354
 * @template T
355
 * @param {(Promise<T> & { status?: string, value?: T, reason?: any }) | import('../../src/internal').PreactContext} resource
356
 * @returns {T}
357
 */
NEW
358
export const use = resource => {
×
359
        // A Context is a function without a `then`, a thenable has one.
360
        if (resource.then) {
3,207✔
361
                if (resource.status == 'fulfilled') return resource.value;
1✔
362
                if (resource.status == 'rejected') throw resource.reason;
11✔
363
                if (!resource.status) {
6✔
364
                        resource.status = 'pending';
11✔
365
                        resource.then(
5✔
366
                                value => {
5✔
367
                                        resource.status = 'fulfilled';
4✔
368
                                        resource.value = value;
3✔
369
                                },
3✔
370
                                reason => {
3✔
371
                                        resource.status = 'rejected';
3✔
372
                                        resource.reason = reason;
4✔
373
                                }
1✔
374
                        );
375
                }
376
                throw resource;
1✔
377
        }
1✔
378

379
        const id = resource._id;
1✔
380
        const provider = currentComponent._globalContext[id];
1✔
381
        if (!provider) return resource._defaultValue;
17✔
382
        // `use` runs on every render without hook state, while `provider.sub`
383
        // wraps `componentWillUnmount` on each call — mark the component so we
384
        // only subscribe it once.
385
        if (!currentComponent[id]) {
6✔
386
                currentComponent[id] = true;
6✔
387
                provider.sub(currentComponent);
6✔
388
        }
389
        return provider.props.value;
17✔
390
};
4✔
391

392
// This is a very very private internal function for React it
393
// is used to sort-of do runtime dependency injection.
394
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
4✔
395
        ReactCurrentDispatcher: {
17✔
396
                current: {
1✔
397
                        readContext: use,
1✔
398
                        useCallback,
1✔
399
                        useContext,
1✔
400
                        useDebugValue,
1✔
401
                        useDeferredValue,
1✔
402
                        useEffect,
1✔
403
                        useId,
1✔
404
                        useImperativeHandle,
1✔
405
                        useInsertionEffect,
1✔
406
                        useLayoutEffect,
1✔
407
                        useMemo,
1✔
408
                        // useMutableSource, // experimental-only and replaced by uSES, likely not worth supporting
409
                        useReducer,
1✔
410
                        useRef,
1✔
411
                        useState,
1✔
412
                        useSyncExternalStore,
1✔
413
                        useTransition
1✔
414
                }
415
        }
416
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc