UNPKG

epic-state

Version:

Reactive state management for frontend libraries.

153 lines (129 loc) 4.83 kB
// @ts-ignore All export paths will be type checked, avoid errors if not installed. import { options } from 'preact' import { log } from '../helper' import { type Plugin, type PluginActions, type Property, type ProxyObject, TupleArrayMap } from '../types' // With preact you can set custom hooks for the render cycle: https://preactjs.com/guide/v10/options // While the render hook isn't exposed it can still be added as '__r' and used to access // the currently rendered component reliably. interface Component<P = {}, S = {}> { setState<K extends keyof S>( state: ((prevState: Readonly<S>, props: Readonly<P>) => Pick<S, K> | Partial<S> | null) | (Pick<S, K> | Partial<S> | null), callback?: () => void, ): void } enum OptionsTypes { // biome-ignore lint/style/useNamingConvention: Preact default. HOOK = '__h', // biome-ignore lint/style/useNamingConvention: Preact default. DIFF = '__b', // biome-ignore lint/style/useNamingConvention: Preact default. DIFFED = 'diffed', // biome-ignore lint/style/useNamingConvention: Preact default. RENDER = '__r', // biome-ignore lint/style/useNamingConvention: Preact default. CATCH_ERROR = '__e', // biome-ignore lint/style/useNamingConvention: Preact default. UNMOUNT = 'unmount', } // biome-ignore lint/style/useNamingConvention: Will lead to infinite parsing when run with --write. interface VNode { /** The component instance for this VNode */ __c: AugmentedComponent /** The parent VNode */ __?: VNode /** The DOM node for this VNode */ __e?: Element | Text /** Props that had Signal values before diffing (used after diffing to subscribe) */ __np?: Record<string, any> | null } interface AugmentedComponent extends Component<any, any> { __v: VNode _updater?: () => void _updateFlags: number } const HAS_PENDING_UPDATE = 1 << 0 function hook<T extends OptionsTypes>(hookName: T, hookFn: HookFn<T>) { // @ts-ignore-next-line private options hooks usage options[hookName] = hookFn.bind(null, options[hookName]) } interface OptionsType { [OptionsTypes.HOOK](component: Component, index: number, type: number): void [OptionsTypes.DIFF](vnode: VNode): void [OptionsTypes.DIFFED](vnode: VNode): void [OptionsTypes.RENDER](vnode: VNode): void [OptionsTypes.CATCH_ERROR](error: any, vnode: VNode, oldNode: VNode): void [OptionsTypes.UNMOUNT](vnode: VNode): void } type HookFn<T extends keyof OptionsType> = (old: OptionsType[T], ...a: Parameters<OptionsType[T]>) => ReturnType<OptionsType[T]> let currentComponent: AugmentedComponent | undefined hook(OptionsTypes.RENDER, (old, vnode) => { const component = vnode.__c if (component) { component._updateFlags &= ~HAS_PENDING_UPDATE if (!component._updater) { component._updater = function forceUpdate() { component._updateFlags |= HAS_PENDING_UPDATE component.setState({}) } } } currentComponent = component if (old) { // NOTE missing in original implementation, sometimes causes issues, probably due to order of registration. old(vnode) } }) hook(OptionsTypes.CATCH_ERROR, (old, error, vnode, oldNode) => { currentComponent = undefined if (old) { old(error, vnode, oldNode) } }) hook(OptionsTypes.DIFFED, (old, vnode) => { currentComponent = undefined if (old) { old(vnode) } }) export const connect: Plugin = (initialize) => { if (initialize !== 'initialize') { log('connect plugin cannot be configured', 'warning') } const observedProperties = new TupleArrayMap<ProxyObject, Property, () => void>() return { set: ({ property, parent, value, previousValue }) => { if (value === previousValue) { return } const components = observedProperties.get(parent, property) // Remove, as get will be tracked again during render. if (observedProperties.has(parent, property)) { observedProperties.delete(parent, property) } // Trigger rerender on components. if (components) { for (const component of components) { component() } } }, get: ({ property, parent }) => { if (!currentComponent) { return // Accessed outside a component. } if (!currentComponent._updater) { log('Missing _updater on component', 'warning') return } // Register rerender on current component. if (observedProperties.has(parent, property)) { const components = observedProperties.get(parent, property) if (!components?.includes(currentComponent._updater)) { components?.push(currentComponent._updater) } } else if (!observedProperties.has(parent, property)) { observedProperties.add(parent, property, currentComponent._updater) } }, } as PluginActions }