preact
Version:
Fast 4kb React-compatible Virtual DOM library.
412 lines (368 loc) • 12.3 kB
JavaScript
import {
render as preactRender,
hydrate as preactHydrate,
options,
toChildArray,
Component
} from 'preact';
import {
useCallback,
useContext,
useDebugValue,
useEffect,
useId,
useImperativeHandle,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState
} from 'preact/hooks';
import { useDeferredValue, useInsertionEffect, useTransition } from './index';
import { assign, IS_NON_DIMENSIONAL } from './util';
export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
const MODE_HYDRATE = 1 << 5;
let currentComponent, hydrationRoot, renderTrackingInitialized;
const CAMEL_PROPS =
/^(?: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]/;
const CAMEL_REPLACE = /[A-Z0-9]/g;
const IS_DOM = typeof document < 'u';
/**
* This is taken from https://github.com/facebook/react/blob/main/packages/use-sync-external-store/src/useSyncExternalStoreShimClient.js#L84
* on a high level this cuts out the warnings, ... and attempts a smaller implementation
* @typedef {{ _value: any; _getSnapshot: () => any }} Store
*/
export const useSyncExternalStore = /* @__PURE__ */ initRenderTracking(
function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
const serverRendering = options._skipEffects || hydrationRoot;
const value = serverRendering
? (getServerSnapshot || getSnapshot)()
: getSnapshot();
/**
* @typedef {{ _instance: Store }} StoreRef
* @type {[StoreRef, (store: StoreRef) => void]}
*/
const [{ _instance }, forceUpdate] = useState({
_instance: { _value: value, _getSnapshot: getSnapshot }
});
useLayoutEffect(() => {
_instance._value = value;
_instance._getSnapshot = getSnapshot;
if (didSnapshotChange(_instance)) {
forceUpdate({ _instance });
}
}, [subscribe, value, getSnapshot]);
useEffect(() => {
if (didSnapshotChange(_instance)) {
forceUpdate({ _instance });
}
return subscribe(() => {
if (didSnapshotChange(_instance)) {
forceUpdate({ _instance });
}
});
}, [subscribe]);
return value;
}
);
/** @type {(inst: Store) => boolean} */
function didSnapshotChange(inst) {
try {
return !Object.is(inst._value, inst._getSnapshot());
} catch (error) {
return true;
}
}
// Input types for which onchange should not be converted to oninput.
const onChangeInputType = type => /fil|che|rad/.test(type);
// Some libraries like `react-virtualized` explicitly check for this.
Component.prototype.isReactComponent = true;
// `UNSAFE_*` lifecycle hooks
// Preact only ever invokes the unprefixed methods.
// Here we provide a base "fallback" implementation that calls any defined UNSAFE_ prefixed method.
// - If a component defines its own `componentDidMount()` (including via defineProperty), use that.
// - If a component defines `UNSAFE_componentDidMount()`, `componentDidMount` is the alias getter/setter.
// - If anything assigns to an `UNSAFE_*` property, the assignment is forwarded to the unprefixed property.
// See https://github.com/preactjs/preact/issues/1941
[
'componentWillMount',
'componentWillReceiveProps',
'componentWillUpdate'
].forEach(key => {
Object.defineProperty(Component.prototype, key, {
configurable: true,
get() {
return this['UNSAFE_' + key];
},
set(v) {
Object.defineProperty(this, key, {
configurable: true,
writable: true,
value: v
});
}
});
});
/**
* Proxy render() since React returns a Component reference.
* @param {import('./internal').VNode} vnode VNode tree to render
* @param {import('./internal').PreactElement} parent DOM node to render vnode tree into
* @param {() => void} [callback] Optional callback that will be called after rendering
* @returns {import('./internal').Component | null} The root component reference or null
*/
export function render(vnode, parent, callback) {
// React destroys any existing DOM nodes, see #1727
// ...but only on the first render, see #1828
if (parent._children == null) {
parent.textContent = '';
}
preactRender(vnode, parent);
if (typeof callback == 'function') callback();
return vnode ? vnode._component : null;
}
export function hydrate(vnode, parent, callback) {
preactHydrate(vnode, parent);
if (typeof callback == 'function') callback();
return vnode ? vnode._component : null;
}
let oldEventHook = options.event;
options.event = e => {
if (oldEventHook) e = oldEventHook(e);
e.persist = () => {};
e.isPropagationStopped = function isPropagationStopped() {
return this.cancelBubble;
};
e.isDefaultPrevented = function isDefaultPrevented() {
return this.defaultPrevented;
};
return (e.nativeEvent = e);
};
const classNameDescriptorNonEnumberable = {
configurable: true,
get() {
return this.class;
}
};
function handleDomVNode(vnode) {
let props = vnode.props,
type = vnode.type,
normalizedProps = {},
isNonDashedType = type.indexOf('-') == -1;
for (let i in props) {
let value = props[i];
if (
(i == 'value' && 'defaultValue' in props && value == null) ||
// Emulate React's behavior of not rendering the contents of noscript tags on the client.
(IS_DOM && i == 'children' && type == 'noscript') ||
i == 'class' ||
i == 'className'
) {
// Skip applying value if it is null/undefined and we already set
// a default value
continue;
}
if (i == 'style' && typeof value == 'object') {
let cloned;
for (let key in value) {
if (typeof value[key] == 'number' && !IS_NON_DIMENSIONAL.test(key)) {
if (!cloned) {
cloned = value = assign({}, value);
}
value[key] += 'px';
}
}
} else if (i == 'defaultValue' && 'value' in props && props.value == null) {
// `defaultValue` is treated as a fallback `value` when a value prop is present but null/undefined.
// `defaultValue` for Elements with no value prop is the same as the DOM defaultValue property.
i = 'value';
} else if (i == 'download' && value === true) {
// Calling `setAttribute` with a truthy value will lead to it being
// passed as a stringified value, e.g. `download="true"`. React
// converts it to an empty string instead, otherwise the attribute
// value will be used as the file name and the file will be called
// "true" upon downloading it.
value = '';
} else if (i == 'translate' && value === 'no') {
value = false;
} else if (i[0] == 'o' && i[1] == 'n') {
let lowerCased = i.toLowerCase();
if (lowerCased == 'ondoubleclick') {
i = 'ondblclick';
} else if (
lowerCased == 'onchange' &&
(type == 'input' || type == 'textarea') &&
!onChangeInputType(props.type)
) {
lowerCased = i = 'oninput';
} else if (lowerCased == 'onfocus') {
i = 'onfocusin';
} else if (lowerCased == 'onblur') {
i = 'onfocusout';
}
// Add support for onInput and onChange, see #3561
// if we have an oninput prop already change it to oninputCapture
if (lowerCased == 'oninput') {
i = lowerCased;
if (normalizedProps[i]) {
i = 'oninputCapture';
}
}
} else if (isNonDashedType && CAMEL_PROPS.test(i)) {
i = i.replace(CAMEL_REPLACE, '-$&').toLowerCase();
} else if (value === null) {
value = undefined;
}
normalizedProps[i] = value;
}
if (type == 'select') {
// Add support for array select values: <select multiple value={[]} />
if (normalizedProps.multiple && Array.isArray(normalizedProps.value)) {
// forEach() always returns undefined, which we abuse here to unset the value prop.
normalizedProps.value = toChildArray(props.children).forEach(child => {
child.props.selected =
normalizedProps.value.indexOf(child.props.value) != -1;
});
}
// Adding support for defaultValue in select tag
if (normalizedProps.defaultValue != null) {
normalizedProps.value = toChildArray(props.children).forEach(child => {
if (normalizedProps.multiple) {
child.props.selected =
normalizedProps.defaultValue.indexOf(child.props.value) != -1;
} else {
child.props.selected =
normalizedProps.defaultValue == child.props.value;
}
});
}
}
if (props.class && !props.className) {
normalizedProps.class = props.class;
Object.defineProperty(
normalizedProps,
'className',
classNameDescriptorNonEnumberable
);
} else if (props.className) {
normalizedProps.class = normalizedProps.className = props.className;
}
vnode.props = normalizedProps;
}
let oldVNodeHook = options.vnode;
options.vnode = vnode => {
// only normalize props on Element nodes
if (typeof vnode.type == 'string') {
handleDomVNode(vnode);
} else if (typeof vnode.type == 'function') {
const shouldApplyRef =
'prototype' in vnode.type && vnode.type.prototype.render;
if ('ref' in vnode.props && shouldApplyRef) {
vnode.ref = vnode.props.ref;
delete vnode.props.ref;
}
if (vnode.type.defaultProps) {
let normalizedProps = assign({}, vnode.props);
for (let i in vnode.type.defaultProps) {
if (normalizedProps[i] === undefined) {
normalizedProps[i] = vnode.type.defaultProps[i];
}
}
vnode.props = normalizedProps;
}
}
// React 19 keeps `ref` in element.props for every element type, and that's
// where libraries detecting React 19 read it (e.g. MUI's getReactElementRef).
// DOM and class vnodes keep theirs on vnode.ref.
if (vnode.ref && !('ref' in vnode.props)) {
Object.defineProperty(vnode.props, 'ref', {
value: vnode.ref,
configurable: true,
writable: true
});
}
vnode.$$typeof = REACT_ELEMENT_TYPE;
if (oldVNodeHook) oldVNodeHook(vnode);
};
// Only needed for react-relay and useSyncExternalStore hydration.
function initRenderTracking(value) {
if (!renderTrackingInitialized) {
renderTrackingInitialized = true;
const oldBeforeRender = options._render;
options._render = vnode => {
if (oldBeforeRender) oldBeforeRender(vnode);
if (vnode._flags & MODE_HYDRATE) hydrationRoot = vnode;
currentComponent = vnode._component;
};
const oldDiffed = options.diffed;
options.diffed = vnode => {
if (oldDiffed) oldDiffed(vnode);
currentComponent = null;
if (hydrationRoot == vnode) hydrationRoot = null;
};
}
return value;
}
/**
* Read the value of a Promise (suspending while pending) or a Context.
* Unlike other hooks, `use` may be called conditionally.
* @template T
* @param {(Promise<T> & { status?: string, value?: T, reason?: any }) | import('../../src/internal').PreactContext} resource
* @returns {T}
*/
export const use = /* @__PURE__ */ initRenderTracking(function use(resource) {
// A Context is a function without a `then`, a thenable has one.
if (resource.then) {
if (resource.status == 'fulfilled') return resource.value;
if (resource.status == 'rejected') throw resource.reason;
if (!resource.status) {
resource.status = 'pending';
resource.then(
value => {
resource.status = 'fulfilled';
resource.value = value;
},
reason => {
resource.status = 'rejected';
resource.reason = reason;
}
);
}
throw resource;
}
const id = resource._id;
const provider = currentComponent._globalContext[id];
if (!provider) return resource._defaultValue;
// `use` runs on every render without hook state, while `provider.sub`
// wraps `componentWillUnmount` on each call — mark the component so we
// only subscribe it once.
if (!currentComponent[id]) {
currentComponent[id] = true;
provider.sub(currentComponent);
}
return provider.props.value;
});
// This is a very very private internal function for React it
// is used to sort-of do runtime dependency injection.
export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
ReactCurrentDispatcher: {
current: {
readContext: use,
useCallback,
useContext,
useDebugValue,
useDeferredValue,
useEffect,
useId,
useImperativeHandle,
useInsertionEffect,
useLayoutEffect,
useMemo,
// useMutableSource, // experimental-only and replaced by uSES, likely not worth supporting
useReducer,
useRef,
useState,
useSyncExternalStore,
useTransition
}
}
};