nexora
Version:
A lightweight, production-ready JavaScript library for building user interfaces, supporting JSX.
161 lines (160 loc) • 5.87 kB
JavaScript
import { render } from '../dom/render';
/**
* ## Reactive State ##
* @description
* - This class is used to manage the state and reactivity of the components.
*/
class ReactiveState {
stateIndexes = new WeakMap();
currentComponentFn = null;
rootElement = null;
states = new WeakMap();
/**
* Creates a state for the given component.
* @param initialValue - The initial value of the state.
* @returns A tuple containing the getter and setter for the state.
*/
createState(initialValue) {
this.rootElement ??= document.getElementById('app');
if (!this.currentComponentFn) {
throw new Error('createState must be called within a component');
}
const componentFn = this.currentComponentFn;
if (!this.stateIndexes.has(componentFn)) {
this.stateIndexes.set(componentFn, 0);
}
const stateIndex = this.stateIndexes.get(componentFn);
this.stateIndexes.set(componentFn, stateIndex + 1);
if (!this.states.has(componentFn)) {
this.states.set(componentFn, new Map());
}
const componentStates = this.states.get(componentFn);
if (!componentStates.has(stateIndex)) {
componentStates.set(stateIndex, initialValue);
}
/**
* Get the state for the given component.
* @returns The state for the given component.
*/
const getState = () => {
const states = this.states.get(componentFn);
return states?.get(stateIndex);
};
/**
* Set the state for the given component.
* @param newValue - The new value of the state.
*/
const setState = (newValue) => {
const states = this.states.get(componentFn);
if (!states)
return;
const currentValue = states.get(stateIndex);
const nextValue = typeof newValue === 'function' ? newValue(currentValue) : newValue;
if (Object.is(currentValue, nextValue)) {
return;
}
states.set(stateIndex, nextValue);
queueMicrotask(() => {
const newVNode = this.render(componentFn);
if (this.rootElement) {
const componentElement = this.findElementByComponent(this.rootElement, componentFn);
if (componentElement) {
const oldVNode = componentElement._vnode;
if (oldVNode) {
render(newVNode, componentElement);
}
}
}
});
};
return [getState, setState];
}
/**
* Checks if the given component function has state.
* @param componentFn - The component function to check.
* @returns True if the component function has state, false otherwise.
*/
hasState(componentFn) {
return this.states.has(componentFn);
}
/**
* Triggers an update for the given component function.
* @param componentFn - The component function to trigger an update for.
*/
triggerUpdate(componentFn) {
queueMicrotask(() => {
const newVNode = this.render(componentFn);
if (this.rootElement) {
const componentElement = this.findElementByComponent(this.rootElement, componentFn);
if (componentElement) {
const oldVNode = componentElement._vnode;
if (oldVNode) {
render(newVNode, componentElement);
}
}
}
});
}
/**
* Finds the element by the given component function.
* @param root - The root element to search within.
* @param componentFn - The component function to search for.
* @returns The element that matches the component function.
*/
findElementByComponent(root, componentFn) {
if (root._vnode?.props?._componentFn === componentFn) {
return root;
}
const searchChildren = (element) => {
for (const child of Array.from(element.children)) {
if (child._vnode?.props?._componentFn === componentFn) {
return child;
}
const found = searchChildren(child);
if (found)
return found;
}
return null;
};
return searchChildren(root);
}
/**
* Renders the given component function.
* @param ComponentFn - The component function to render.
* @returns The VNode of the component.
*/
render(ComponentFn, props) {
const prevComponent = this.currentComponentFn;
this.currentComponentFn = ComponentFn;
try {
this.stateIndexes.set(ComponentFn, 0);
const result = ComponentFn(props || {});
const renderKey = Date.now();
// clean up any temporary references
if (result && typeof result === 'object') {
Object.keys(result).forEach((key) => {
if (key.startsWith('_temp')) {
delete result[key];
}
});
}
return {
type: 'reactive-wrapper',
props: {
children: [result],
_componentFn: ComponentFn,
_renderKey: renderKey,
},
key: null,
ref: null,
};
}
finally {
this.currentComponentFn = prevComponent;
}
}
}
export const reactive = new ReactiveState();
export function createState(initialValue) {
return reactive.createState(initialValue);
}