UNPKG

classy-solid

Version:

Solid.js reactivity patterns for classes, and class components.

177 lines (163 loc) 5.96 kB
import type {SignalMetadata} from './types.js' import { finalizeMemos, getMemberStat, getSignalsAndMemos, isPriorSignalOrMemoDefined, memoifyIfNeeded, sortSignalsMemosInMetadata, } from '../_state.js' /** * A decorator that make a signal property derived from a memoized computation * based on other signals. Effects depending on this property will re-run when * the computed value changes, but not if the computed value stays the same even * if the dependencies changed. * * @example * ```ts * import {reactive, signal, memo} from "classy-solid"; * * @reactive * class Example { * @signal a = 1 * @signal b = 2 * * // @memo can be used on a field, getter, or accessor. * * // Writable memo via field (requires function to have a parameter (arity > 0)) * @memo sum = (v?: number) => this.a + this.b * * // Readonly memo via getter only * @memo get sum2() { * return this.a + this.b * } * * // Readonly memo via accessor (requires arrow function, not writable because no parameter (arity = 0)) * @memo accessor sum3 = () => this.a + this.b * * // Readonly memo via method * @memo sum4() { * return this.a + this.b * } * * // Writable memo via getter with setter * @memo get sum5() { * return this.a + this.b * } * @memo set sum5(value: number) { * // empty setter makes it writable (logic in here will be ignored if any) * } * * // The following variants are not supported yet as no runtime or TS support exists yet for the syntax. * * // Writable memo via accessor, alternative long-hand syntax (not yet released, no runtime or TS support yet) * @memo accessor sum6 { get; set } = () => this.a + this.b * * // Readonly memo via accessor with only getter (not released yet, no runtime or TS support yet) * @memo accessor sum7 { get; } = () => this.a + this.b * * // Readonly memo via accessor with only getter, alternative syntax (not released yet, no runtime or TS support yet) * @memo accessor sum8 { * get() { * return this.a + this.b * } * } * * // Readonly memo via accessor with only getter, alternative syntax (not released yet, no runtime or TS support yet) * @memo accessor sum8 { * get() { * return this.a + this.b * } * set(_v: number) { * // empty setter makes it writable (logic in here will be ignored if any) * } * } * } * * const ex = new Example(); * * console.log(ex.sum(), ex.sum2, ex.sum3(), ex.sum4(), ex.sum5); // 3 3 3 3 3 * * createEffect(() => { * console.log(ex.sum(), ex.sum2, ex.sum3(), ex.sum4(), ex.sum5); * }); * * ex.a = 5; // Logs: 7 7 7 7 7 * * // This won't log anything since the computed memo values don't change (all still 7). * batch(() => { * ex.a = 3; * ex.b = 4; * }) * * ex.sum(20); // Logs: 20 7 7 7 7 (only sum is updated) * * ex.sum5 = 15; // Logs: 20 7 7 7 15 (only sum5 is updated) * * ex.sum2 = 10; // Runtime error: Cannot set readonly property "sum2". * ``` */ export function memo( _value: | undefined | ((val?: any) => any) // writable memo via field or method | (() => any) // readonly memo via field or method | ((val?: any) => void) // memo setter | (() => void) // memo getter | ClassAccessorDecoratorTarget<unknown, () => any> // today's auto-accessors, readonly memo | ClassAccessorDecoratorTarget<unknown, (val?: number) => any>, // today's auto-accessors, writable memo context: | ClassFieldDecoratorContext | ClassGetterDecoratorContext | ClassSetterDecoratorContext | ClassAccessorDecoratorContext | ClassMethodDecoratorContext, ) { if (context.static) throw new Error('@memo is not supported on static fields yet.') const {kind, name} = context const metadata = context.metadata as SignalMetadata const signalsAndMemos = getSignalsAndMemos(metadata) if (kind === 'field') { const stat = getMemberStat(name, 'memo-field', signalsAndMemos) context.addInitializer(function () { sortSignalsMemosInMetadata(metadata) memoifyIfNeeded(this as object, name, stat) // If we skipped memoifying prior memo members (accessor and method // memos) because of prior signal-fields, memo-fields, or // memo-auto-accessors, finalize those memos now. finalizeMemos(this as object, stat, signalsAndMemos) }) } else if (kind === 'accessor') { const stat = getMemberStat(name, 'memo-auto-accessor', signalsAndMemos) context.addInitializer(function () { sortSignalsMemosInMetadata(metadata) memoifyIfNeeded(this as object, name, stat, true) // If we skipped memoifying prior memo members (accessor and method // memos) because of prior signal-fields, memo-fields, or // memo-auto-accessors, finalize those memos now. finalizeMemos(this as object, stat, signalsAndMemos) }) } else if (kind === 'method') { const stat = getMemberStat(name, 'memo-method', signalsAndMemos) context.addInitializer(function () { sortSignalsMemosInMetadata(metadata) // If any signal-fields, memo-fields, or memo-auto-accessors are // defined on the class (thus sorted before this memo method), skip // memoifying now because we need those to be initialized first, // then we'll memoify after that. if (isPriorSignalOrMemoDefined(this as object, name, signalsAndMemos)) return memoifyIfNeeded(this as object, name, stat) }) } else if (kind === 'getter' || kind === 'setter') { const stat = getMemberStat(name, 'memo-accessor', signalsAndMemos) context.addInitializer(function () { sortSignalsMemosInMetadata(metadata) // If any signal-fields, memo-fields, or memo-auto-accessors are // defined on the class (thus sorted before this memo method), skip // memoifying now because we need those to be initialized first, // then we'll memoify after that. if (isPriorSignalOrMemoDefined(this as object, name, signalsAndMemos)) return memoifyIfNeeded(this as object, name, stat) }) } }