/* * This file belongs to Hoist, an application development toolkit * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) * * Copyright © 2026 Extremely Heavy Industries Inc. */ import {instanceManager} from '@xh/hoist/core/impl/InstanceManager'; import { CreatesSpec, UsesSpec, HoistProps, DefaultHoistProps, elementFactory, ElementFactory, TestSupportProps, PlainObject } from './'; import { useModelLinker, localModelContext, ModelLookup, ModelLookupContext, modelLookupContextProvider, ModelSpec, uses, formatSelector, HoistModel } from './model'; import {logError, throwIf, warnIf, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps, useOnMount, useOnUnmount} from '@xh/hoist/utils/react'; import classNames from 'classnames'; import {isFunction, isPlainObject, isObject} from 'lodash'; import {observer} from '../mobx'; import { ForwardedRef, forwardRef, FC, memo, ReactNode, useContext, useEffect, useRef, createElement, FunctionComponent, useDebugValue } from 'react'; /** * Type representing props passed to a HoistComponent's render function. * * This type removes from its base type several properties that are pulled out by the HoistComponent itself and * not provided to the render function. `modelConfig` and `modelRef` are resolved into the `model` property. * `ref` is passed as the second argument to the render function. */ export type RenderPropsOf
= P & { /** Pre-processed by HoistComponent internals into a mounted model. Never passed to render. */ modelConfig: never; /** Pre-processed by HoistComponent internals and attached to model. Never passed to render. */ modelRef: never; /** Pre-processed by HoistComponent internals and passed as second argument to render. */ ref: never; }; /** * Configuration for creating a Component. May be specified either as a render function, * or an object containing a render function and associated metadata. */ export type ComponentConfig
= | ((props: RenderPropsOf
, ref?: ForwardedRef , ref?: ForwardedRef | false;
/**
* Base CSS class for this component. Will be combined with any className
* in props, with the combined string passed into render as a prop.
*/
className?: string;
/** Component name for debugging/inspection. */
displayName?: string;
/**
* True (default) to wrap component in a call to `React.memo()`.
* Components that are known to be unable to make effective use of memo (e.g. container
* components) may set this to `false`. Not typically set by application code.
*/
memo?: boolean;
/**
* True (default) to enable MobX-powered reactivity via the
* `observer()` HOC from mobx-react. Components that are known to dereference no observable
* state may set this to `false`, but this is not typically done by application code.
*/
observer?: boolean;
/**
* Static, app-wide configuration for the component, attached to the returned component
* as a typed `defaults` object that applications can modify at bootstrap to customize
* behavior for all instances (e.g. `Button.defaults.minimal = false` in Bootstrap.ts).
*
* Most commonly used to supply default values for selected props. May also be used for
* other app-overridable settings that are not exposed as props.
*/
defaults?: D;
};
let cmpIndex = 0; // index for anonymous component dispay names
/**
* The primary entry point for defining Hoist components - functional React components enhanced
* with MobX reactivity and integrated model support.
*
* Accepts a configuration object (or a bare render function) and returns a React functional
* component. The `model` config is the key option: use {@link creates} to have the component
* create and own its backing model, {@link uses} to source a model from props or context, or
* `false` for simple components with no model. Defaults to `uses('*')` if not specified.
*
* Components are wrapped in the MobX `observer` HOC by default, enabling automatic re-rendering
* when observable state read during render changes.
*
* Forward refs ({@link https://reactjs.org/docs/forwarding-refs.html}) are supported by
* specifying a render function with two arguments - the second is treated as a ref, and
* `React.forwardRef` is applied automatically.
*
* Most components should be defined via one of two convenience methods rather than calling
* this function directly:
* - `hoistCmp.factory()` - returns an element factory (the standard pattern for app components).
* - `hoistCmp.withFactory()` - returns a `[Component, factory]` pair (the standard pattern for
* library components that need to export both).
*
* See the core package README (`core/README.md`) for full documentation on component
* configuration, model specs, and context lookup behavior.
*
* Components can also declare a typed `defaults` object in their config to expose static,
* app-wide configuration - most typically default values for selected props, but also other
* settings designed to be overridden by applications at bootstrap (e.g.
* `Button.defaults.minimal = false` in Bootstrap.ts). When specified, the returned component
* exposes a typed `defaults` property.
*
* @param config - specification object, or a render function defining the component.
* @returns a functional React Component for use within Hoist apps.
*/
export function hoistCmp (
config: ComponentConfig
): FC & {defaults: D};
export function hoistCmp (config: ComponentConfig ): FC ;
export function hoistCmp (config: ComponentConfig ): FC {
// 0) Pre-process/parse args.
if (isFunction(config)) config = {render: config, displayName: config.name};
// 1) Default and validate the modelSpec.
const modelSpec = withDefault(config.model, uses('*'));
throwIf(
modelSpec && !(modelSpec instanceof UsesSpec) && !(modelSpec instanceof CreatesSpec),
"The 'model' config passed to hoistComponent() is incorrectly specified: provide a spec returned by either uses() or creates()."
);
let render = config.render,
cfg: Config = {
className: config.className,
displayName: config.displayName ? config.displayName : 'HoistCmp' + cmpIndex++,
isMemo: withDefault(config.memo, true),
isObserver: withDefault(config.observer, true),
isForwardRef: render.length === 2,
modelSpec: modelSpec ? modelSpec : null,
defaults: config.defaults ?? null
};
warnIf(
!cfg.isMemo && cfg.isObserver,
'Cannot create an observer component without `memo`. Memo is built-in to MobX observable. Component will be memoized.'
);
// 2) Wrap supplied render function with model and classname, support. Be sure to clone props.
if (cfg.modelSpec) {
render = wrapWithModel(render, cfg);
}
if (cfg.className) {
render = wrapWithClassName(render, cfg);
}
if (cfg.modelSpec || cfg.className) {
render = wrapWithClonedProps(render);
}
// 4) Wrap with standard react HOCs, mark, install defaults if specified, and return.
let ret = render as any;
ret.displayName = cfg.displayName;
if (cfg.isForwardRef) ret = forwardRef(ret);
if (cfg.isObserver) ret = observer(ret);
if (cfg.isMemo && !cfg.isObserver) ret = memo(ret);
ret.displayName = cfg.displayName;
ret.isHoistComponent = true;
if (cfg.defaults) ret.defaults = cfg.defaults;
return ret;
}
/**
* Alias for {@link hoistCmp}.
*/
export const hoistComponent = hoistCmp;
/**
* Return an element factory for a newly defined component.
*
* Most typically used by application, this provides a simple element factory.
*/
export function hoistCmpFactory (
config: ComponentConfig
): ElementFactory ;
export function hoistCmpFactory(config) {
return elementFactory(hoistCmp(config));
}
hoistCmp.factory = hoistCmpFactory;
/**
* Returns a 2-element list containing both the newly defined Component and an elementFactory for it.
* Used by Hoist for exporting Component artifacts that support both JSX and elementFactory based development.
*
* Not typically used by applications.
*/
export function hoistCmpWithFactory (
config: ComponentConfig
): [FC & {defaults: D}, ElementFactory ];
export function hoistCmpWithFactory (
config: ComponentConfig
): [FC , ElementFactory ];
export function hoistCmpWithFactory(config) {
const cmp = hoistCmp(config);
return [cmp, elementFactory(cmp)];
}
hoistCmp.withFactory = hoistCmpWithFactory;
//----------------------------
// Implementation
//----------------------------
//----------------------------------
// internal types and core wrappers
//----------------------------------
type RenderFn = (props: HoistProps & TestSupportProps, ref?: ForwardedRef