Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | 1x 1x 1x 1x 1x 1x 1x 14x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 14x 3x 3x 14x 14x 14x 14x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 14x 14x 3x 3x 3x 3x 3x 14x 14x 14x 14x 1x 10x 10x 10x 10x 10x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 10x 10x 10x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 9x 9x 9x 6x 3x 3x 6x 9x 9x 9x 6x 6x 6x 6x 6x 6x 9x 9x 8x 8x 8x 8x 1x 6x 6x 1x 1x 1x 1x 1x 1x 1x 6x 6x 1x 1x 1x 1x 1x | import { anchor, captureStack, createObserver, createStack, microtask, withStack } from '@anchorlib/core';
import type { FunctionComponent, ReactNode } from 'react';
import { createEffect, createState, memoize } from './hooks.js';
import { createLifecycle } from './lifecycle.js';
import { getProps, setupProps, withProps } from './props.js';
import type { SetupComponent, SetupProps, Snippet, StableComponent, Template } from './types.js';
const RENDERER_INIT_VERSION = 1;
const CLEANUP_DEBOUNCE_TIME = 0;
/**
* Higher-Order Component that creates a stable setup component following the modern component lifecycle.
*
* The `setup` HOC implements a modern rendering approach where components render once and maintain
* stable behavior. It ensures that:
* - The component only re-renders when its props actually change
* - Lifecycle events (mount/unmount) are properly managed
* - Setup and cleanup operations are handled automatically
*
* This HOC aligns with the modern component lifecycle philosophy where components render once
* and updates are controlled through explicit prop changes rather than internal state.
*
* @template C - The component type being wrapped
* @param {C} Component - The function component to wrap with stable lifecycle management
* @param {string} [displayName] - Optional display name for debugging purposes
* @returns {C} A memoized component that only re-renders when props change
*/
export function setup<P>(Component: SetupComponent<P>, displayName?: string): StableComponent<P> {
if (typeof Component !== 'function') {
const error = new Error('Component must be a function.');
captureStack.violation.general(
'Setup factory violation detected:',
'Attempted to use setup() HOC on a non-functional component.',
error,
undefined,
setup
);
const Factory = () => error.message;
Factory.displayName = `Error(${displayName || 'Anonymous'})`;
return Factory as StableComponent<P>;
}
if (displayName && !(Component as FunctionComponent).displayName) {
(Component as FunctionComponent).displayName = displayName;
}
const componentName = displayName || (Component as FunctionComponent).displayName || Component.name || 'Anonymous';
const render = Component as (props: unknown) => ReactNode;
const propsMap = new WeakMap();
const Factory = (currentProps: Record<string, unknown>) => {
const [[scheduleMount, cancelMount]] = createState(() => microtask(CLEANUP_DEBOUNCE_TIME));
const [[scheduleCleanup, cancelCleanup]] = createState(() => microtask(CLEANUP_DEBOUNCE_TIME));
const [scope] = createState(() => createStack());
const [lifecycle] = createState(() => createLifecycle());
const [baseProps] = createState(() => anchor({ ...currentProps }, { recursive: false }));
propsMap.set(currentProps, baseProps);
const props = setupProps(baseProps);
createEffect(() => {
cancelMount();
cancelCleanup();
scheduleMount(() => {
lifecycle.mount();
});
return () => {
scheduleCleanup(() => {
// @important Actual cleanup should be scheduled to prevent cleaning up the current effects in strict-mode.
propsMap.delete(currentProps);
lifecycle.cleanup();
scope.states.clear();
});
};
}, []);
scope.index = 0;
/**
* Cleanup the previous effect handlers to make sure each render is isolated.
* This also necessary to cover HMR (Fast Refresh) where re-render is expected.
*/
lifecycle.cleanup();
return withStack(scope, () => {
return withProps(props, () => {
return lifecycle.render(() => {
return render(props);
});
});
});
};
Factory.displayName = `Setup(${componentName})`;
const Setup = memoize(Factory, (prevProps, nextProps) => {
const prevPropsRef = propsMap.get(prevProps);
if (prevPropsRef) {
anchor.assign(prevPropsRef, nextProps);
}
return true;
});
Setup.displayName = componentName;
return Setup as StableComponent<P>;
}
/**
* Creates a reactive snippet that has access to parent component's props and context.
*
* The `snippet` HOC creates a reactive view that can be used within a setup component.
* It has access to the parent component's props through closure, making it ideal for
* creating smaller reactive pieces within a larger component.
*
* Key characteristics:
* - Scoped to the parent component's context
* - Has access to parent component's props
* - Reactive to state changes within its scope
* - Automatically manages its own lifecycle and cleanup
*
* @template P - The props type for the snippet
* @template SP - The setup props type extending SetupProps
* @param {Snippet<P, SP>} factory - A function that receives props and parent props, returning React nodes
* @param {string} [displayName] - Optional display name for debugging purposes
* @param strictScope - Whether to force a strict scope for the snippet (internal use).
* @returns {FunctionComponent<P>} A memoized functional component that re-executes when dependencies change
*/
export function snippet<P, SP extends SetupProps = SetupProps>(
factory: Snippet<P, SP>,
displayName?: string,
strictScope = true
): FunctionComponent<P> {
if (typeof factory !== 'function') {
const error = new Error('Renderer must be a function.');
captureStack.violation.general(
'View factory violation detected:',
'Attempted to use view() HOC on a non-functional component.',
error,
undefined,
snippet
);
const Template = () => error.message;
Template.displayName = `Error(${displayName || 'Anonymous'})`;
return Template;
}
const viewName = displayName || (factory as FunctionComponent).name || 'Anonymous';
const parentProps = getProps<SP>();
if (!parentProps && strictScope) {
const error = new Error('Out of component scope.');
captureStack.violation.general(
'Snippet violation detected:',
'Attempted to use snippet() HOC outside of a component.',
error,
[
'Snippet must be declared inside a component (setup)',
'- Use template if the view is meant to be reusable across application',
],
snippet
);
}
const Template = memoize((props: P) => {
const [[scheduleCleanup, cancelCleanup]] = createState(() => microtask(CLEANUP_DEBOUNCE_TIME));
const [, setVersion] = createState(RENDERER_INIT_VERSION);
const [observer] = createState(() => {
return createObserver(() => {
observer.reset();
setVersion((c) => c + 1);
});
});
observer.name = `View(${viewName})`;
createEffect(() => {
cancelCleanup();
return () => {
scheduleCleanup(() => {
observer.destroy();
});
};
}, []);
return observer.run(() => factory(props as P, parentProps as SP));
});
Template.displayName = `View(${viewName})`;
return Template as FunctionComponent<P>;
}
/**
* Creates a standalone reactive template component that relies only on its own props.
*
* The `template` HOC creates a reactive view that is independent of parent context.
* It functions as a standalone reactive component that responds only to changes in
* its own props, making it reusable across different parts of an application.
*
* Key characteristics:
* - Standalone component with no dependency on parent context
* - Relies purely on its own props for reactivity
* - Can be used anywhere in the application like a regular component
* - Maintains its own reactive lifecycle
*
* @template P - The props type for the template
* @param {Template<P>} factory - A function that receives props and returns React nodes
* @param {string} [displayName] - Optional display name for debugging purposes
* @returns {FunctionComponent<P>} A memoized functional component that re-executes when its props change
*/
export function template<P>(factory: Template<P>, displayName?: string): FunctionComponent<P> {
const parentProps = getProps();
if (parentProps) {
captureStack.warning.external(
'Using template inside a component',
'Template should not be used inside a component. Use snippet instead for better clarity of the concern.',
['Template should be used outside of a component'].join('\n'),
template
);
}
return snippet(factory, displayName, false) as FunctionComponent<P>;
}
/**
* @deprecated Use `template` instead. This alias will be removed in a future version.
*/
export const view = template;
/**
* Higher-Order Component that creates and immediately renders a reactive component.
*
* The `render` function combines the functionality of `template` with immediate execution,
* creating a reactive renderer and returning its rendered output. This is useful when you
* want to create and render a reactive component in a single step rather than defining
* it separately and then using it in JSX.
*
* This function follows the same reactive principles as `template`, responding to state
* changes and maintaining the modern component lifecycle approach.
*
* @param {Snippet<SetupProps>} Component - A function that receives props and returns React nodes
* @param {string} [displayName] - Optional display name for debugging purposes
* @returns {ReactNode} The rendered output of the reactive component
*/
export function render(Component: Snippet<never>, displayName?: string): ReactNode {
const Snippet = snippet<Record<string, unknown>>(Component as Snippet<unknown>, displayName);
return <Snippet />;
}
|