From 3547692c9c7510de23f6177a04c9b271c94f4235 Mon Sep 17 00:00:00 2001 From: Constantin Dumitrescu Date: Thu, 20 Oct 2022 10:21:13 +0300 Subject: [PATCH 01/19] feat: add support for refining syntax (get) (#134) * add capability for detecting refining syntax * finalise refining for get * add support get refining in producer --- .../engine.babel-plugin-syntax/package.json | 2 +- .../specs/__snapshots__/api.spec.ts.snap | 106 ++++++++++++++++++ .../specs/api.spec.ts | 12 ++ .../src/compilers/pathCompiler.ts | 30 +++++ .../src/parsers/valueParser.ts | 40 +++++++ .../engine.producer/specs/producer.spec.ts | 35 ++++++ .../src/graph/getInvokablePath.ts | 4 +- .../engine.producer/src/graph/getOperation.ts | 10 +- packages/engine.producer/src/graph/graph.ts | 9 +- .../engine.producer/src/graph/refineGet.ts | 35 ++++++ .../src/graph/updateOperation.ts | 47 ++++---- packages/engine.types/src/producer.ts | 77 +++++++++---- packages/engine.utils/src/now.ts | 4 + 13 files changed, 355 insertions(+), 56 deletions(-) create mode 100644 packages/engine.producer/src/graph/refineGet.ts diff --git a/packages/engine.babel-plugin-syntax/package.json b/packages/engine.babel-plugin-syntax/package.json index 8f3180d7..6330562e 100644 --- a/packages/engine.babel-plugin-syntax/package.json +++ b/packages/engine.babel-plugin-syntax/package.json @@ -6,7 +6,7 @@ "typings": "dist/index.d.ts", "scripts": { "build": "tsc", - "test:simple": "NODE_ENV=test jest --runInBand --config ./jest.config.js --runTestsByPath ./specs/producer*", + "test:simple": "NODE_ENV=test jest --runInBand --config ./jest.config.js --runTestsByPath ./specs/api*", "test:update": "jest --runInBand --config ./jest.config.js --runTestsByPath ./specs/* --updateSnapshot", "test:coverage": "jest --runInBand --config ./jest.config.js --coverageDirectory='./coverage' --collectCoverage --collectCoverageFrom='[\"./src/**/*.{ts,tsx,js,jsx}\"]' --runTestsByPath ./specs/*", "test": "yarn run test:coverage" diff --git a/packages/engine.babel-plugin-syntax/specs/__snapshots__/api.spec.ts.snap b/packages/engine.babel-plugin-syntax/specs/__snapshots__/api.spec.ts.snap index 4c85e354..66fa900b 100644 --- a/packages/engine.babel-plugin-syntax/specs/__snapshots__/api.spec.ts.snap +++ b/packages/engine.babel-plugin-syntax/specs/__snapshots__/api.spec.ts.snap @@ -241,6 +241,112 @@ const result = { }; +`; + +exports[`engine.babel-plugin-syntax should support get refining: should support get refining 1`] = ` + +const result: producer = ({ + a = get.foo.value(), + b = get.foo[param.id].value({ id: 123 }), + c = get.foo.length(), + d = get.foo.includes("foo"), +}) => { +} + + ↓ ↓ ↓ ↓ ↓ ↓ + +const result = { + fn: ({ a, b, c, d }) => {}, + props: { + type: "STRUCT", + value: { + a: { + type: "GET", + path: [ + { + type: "CONST", + value: "foo", + }, + { + type: "REFINEE", + value: { + type: "value", + args: [], + }, + }, + ], + }, + b: { + type: "GET", + path: [ + { + type: "CONST", + value: "foo", + }, + { + type: "INVOKE", + path: ["id"], + }, + { + type: "REFINEE", + value: { + type: "value", + args: [ + { + type: "CONST", + value: { + id: 123, + }, + }, + ], + }, + }, + ], + }, + c: { + type: "GET", + path: [ + { + type: "CONST", + value: "foo", + }, + { + type: "REFINEE", + value: { + type: "length", + args: [], + }, + }, + ], + }, + d: { + type: "GET", + path: [ + { + type: "CONST", + value: "foo", + }, + { + type: "REFINEE", + value: { + type: "includes", + args: [ + { + type: "CONST", + value: "foo", + }, + ], + }, + }, + ], + }, + }, + }, + type: "producer", + buildId: "unique_id", +}; + + `; exports[`engine.babel-plugin-syntax should support get: should support get 1`] = ` diff --git a/packages/engine.babel-plugin-syntax/specs/api.spec.ts b/packages/engine.babel-plugin-syntax/specs/api.spec.ts index 96686932..d2e46fef 100644 --- a/packages/engine.babel-plugin-syntax/specs/api.spec.ts +++ b/packages/engine.babel-plugin-syntax/specs/api.spec.ts @@ -130,6 +130,18 @@ pluginTester({ `, snapshot: true, }, + "should support get refining": { + code: ` + const result: producer = ({ + a = get.foo.value(), + b = get.foo[param.id].value({ id: 123 }), + c = get.foo.length(), + d = get.foo.includes("foo"), + }) => { + } + `, + snapshot: true, + }, "should support constructors": { code: ` const result: producer = ({ diff --git a/packages/engine.babel-plugin-syntax/src/compilers/pathCompiler.ts b/packages/engine.babel-plugin-syntax/src/compilers/pathCompiler.ts index 54ffd142..9d0c0026 100644 --- a/packages/engine.babel-plugin-syntax/src/compilers/pathCompiler.ts +++ b/packages/engine.babel-plugin-syntax/src/compilers/pathCompiler.ts @@ -27,6 +27,36 @@ export const pathCompiler = ( } else if (x.type === ValueTypes.INVOKE) { const path = x.path.map((y: string) => t.stringLiteral(y)); value = t.objectProperty(t.identifier("path"), t.arrayExpression(path)); + } else if (x.type === ValueTypes.REFINEE) { + const type = t.objectProperty( + t.identifier("type"), + t.stringLiteral(x.value.type) + ); + const args = t.objectProperty( + t.identifier("args"), + t.arrayExpression( + x.value.args.map((x) => { + const type = t.objectProperty( + t.identifier("type"), + t.stringLiteral(x.type) + ); + let value; + if (x.type === ValueTypes.CONST) { + value = t.objectProperty(t.identifier("value"), x.value); + } else { + value = t.objectProperty( + t.identifier("path"), + t.arrayExpression(x.path.map((y: string) => t.stringLiteral(y))) + ); + } + return t.objectExpression([type, value]); + }) + ) + ); + value = t.objectProperty( + t.identifier("value"), + t.objectExpression([type, args]) + ); } return t.objectExpression([type, value]); }); diff --git a/packages/engine.babel-plugin-syntax/src/parsers/valueParser.ts b/packages/engine.babel-plugin-syntax/src/parsers/valueParser.ts index c5c0e718..89374520 100644 --- a/packages/engine.babel-plugin-syntax/src/parsers/valueParser.ts +++ b/packages/engine.babel-plugin-syntax/src/parsers/valueParser.ts @@ -18,6 +18,8 @@ import { FuncOperation, StaticOperation, PathType, + AccessMethods, + UpdateMethods, } from "@c11/engine.types"; import { getMemberExpressionParams } from "../utils/getMemberExpressionParams"; import { invokablePathValueParser } from "./invokablePathValueParser"; @@ -132,6 +134,44 @@ const Values: Values = { return constValue({ __node__: node }); } }, + CallExpression: (babel, node: Babel.types.CallExpression) => { + const result = Values.MemberExpression(babel, node.callee); + + if ( + result && + (result.type === OperationTypes.GET || + result.type === OperationTypes.OBSERVE || + result.type === OperationTypes.UPDATE) && + result.path.length > 0 + ) { + const lastIdx = result.path.length - 1; + const last = result.path[lastIdx]; + + if ( + last.type !== ValueTypes.CONST || + (last.value && last.value.__node__) + ) { + throw new Error(`refining ${result.type} does not support expressions`); + } + + //TODO: throw if it's not an approved keyword + + result.path[lastIdx] = { + type: ValueTypes.REFINEE, + value: { + type: last.value, + args: node.arguments.map((x) => { + // TODO: process arguments properly -> could be arg, prop + return { + type: ValueTypes.CONST, + value: x, + }; + }), + }, + }; + } + return result; + }, // foo = get.foo || get.bar LogicalExpression: (babel, node) => { return logicalExpression(babel, node); diff --git a/packages/engine.producer/specs/producer.spec.ts b/packages/engine.producer/specs/producer.spec.ts index 4b35f2bf..b12bff88 100644 --- a/packages/engine.producer/specs/producer.spec.ts +++ b/packages/engine.producer/specs/producer.spec.ts @@ -1186,6 +1186,41 @@ test("should keep arg references for path updates in async processes", (done) => done(); }); +test("should support get refining", () => { + const state = { + foo: 123, + bar: [1, 2, 3], + baz: { + id1: 321, + id2: [1, 2], + id3: "foobar", + }, + id: "id3", + }; + const mock = jest.fn((x) => x); + const struct: producer = ({ + foo = get.foo.value(), + bar = get.bar.length(), + has3 = get.bar.includes(3), + id1 = get.baz[param.id].value({ id: "id1" }), + id2len = get.baz[param.id].length({ id: "id2" }), + id2has = get.baz[param.id].includes(2, { id: "id2" }), + idlink = observe.id, + id3link = get.baz[arg.idlink].value(), + }) => { + mock({ foo, bar, has3, id1, id2len, id2has, id3link }); + }; + run(struct, state, {}); + jest.runAllTimers(); + expect(mock.mock.calls[0][0].foo).toBe(123); + expect(mock.mock.calls[0][0].bar).toBe(3); + expect(mock.mock.calls[0][0].has3).toBe(true); + expect(mock.mock.calls[0][0].id1).toBe(321); + expect(mock.mock.calls[0][0].id2len).toBe(2); + expect(mock.mock.calls[0][0].id2has).toBe(true); + expect(mock.mock.calls[0][0].id3link).toBe("foobar"); +}); + // Add test that checks that references are kept /* diff --git a/packages/engine.producer/src/graph/getInvokablePath.ts b/packages/engine.producer/src/graph/getInvokablePath.ts index dcd323a4..d334ede7 100644 --- a/packages/engine.producer/src/graph/getInvokablePath.ts +++ b/packages/engine.producer/src/graph/getInvokablePath.ts @@ -3,6 +3,7 @@ import { UpdateOperation, GetOperation, OperationParams, + ValueTypes, } from "@c11/engine.types"; import isArray from "lodash/isArray"; import isString from "lodash/isString"; @@ -16,7 +17,8 @@ export const getInvokablePath = ( op: GetOperation | UpdateOperation, params: OperationParams ) => { - const path = op.path.reduce((acc, x: any) => { + const noRefinee = op.path.filter((x) => !x || x.type !== ValueTypes.REFINEE); + const path = noRefinee.reduce((acc, x: any) => { const value = resolveValue(structure, x, params); if (value && value.__symbol__ === PathSymbol) { const expanded = value.__expand__(); diff --git a/packages/engine.producer/src/graph/getOperation.ts b/packages/engine.producer/src/graph/getOperation.ts index 40c46491..c25ee9c6 100644 --- a/packages/engine.producer/src/graph/getOperation.ts +++ b/packages/engine.producer/src/graph/getOperation.ts @@ -4,7 +4,7 @@ import { GetOperation, OperationParams, ProducerContext, - GetValueMethods, + AccessMethods, } from "@c11/engine.types"; import { randomId } from "@c11/engine.utils"; import isString from "lodash/isString"; @@ -19,6 +19,8 @@ import isFunction from "lodash/isFunction"; export const GetOperationSymbol = Symbol("get"); +// TODO: add type for getOperation -> GetOperationRuntime + export const getOperation = ( db: DatastoreInstance, structure: GraphStructure, @@ -66,9 +68,9 @@ export const getOperation = ( // getFoo.is({ type: object, properties: { foo: { type: string }}}}) // schema const operation = { - [GetValueMethods.VALUE]: value, - [GetValueMethods.INCLUDES]: includes, - [GetValueMethods.LENGTH]: length, + [AccessMethods.value]: value, + [AccessMethods.includes]: includes, + [AccessMethods.length]: length, __operation__: { id: randomId(), symbol: GetOperationSymbol, diff --git a/packages/engine.producer/src/graph/graph.ts b/packages/engine.producer/src/graph/graph.ts index 97e37318..d9073c43 100644 --- a/packages/engine.producer/src/graph/graph.ts +++ b/packages/engine.producer/src/graph/graph.ts @@ -30,6 +30,8 @@ import { wildcard } from "../wildcard"; import { serializeProps } from "./serializeProps"; import { isDataEqual } from "./isDataEqual"; import { cloneCbData } from "./cloneCbData"; +import { GetOperationSymbol } from "./getOperation"; +import { refineGet } from "./refineGet"; export class Graph { private structure: GraphStructure; @@ -190,9 +192,12 @@ export class Graph { } else if (refs.includes(`internal.${x}`) || isFunction(data[x])) { acc[x] = value; } else { - acc[x] = cloneDeep(value); + if (value?.__operation__?.symbol === GetOperationSymbol) { + acc[x] = refineGet(this.structure, value); + } else { + acc[x] = cloneDeep(value); + } } - return acc; }, {}); } diff --git a/packages/engine.producer/src/graph/refineGet.ts b/packages/engine.producer/src/graph/refineGet.ts new file mode 100644 index 00000000..bfea86b3 --- /dev/null +++ b/packages/engine.producer/src/graph/refineGet.ts @@ -0,0 +1,35 @@ +import { + AccessMethods, + GraphStructure, + InvokableValue, + ValueTypes, +} from "@c11/engine.types"; +import { GetOperationSymbol } from "./getOperation"; +import { resolveValue } from "./resolveValue"; + +//TODO: add proper types +export const refineGet = (structure: GraphStructure, op: any): any => { + if (op?.__operation__?.symbol !== GetOperationSymbol) { + return op; + } + + const refine: InvokableValue = + op.__operation__.path[op.__operation__.path.length - 1]; + if (refine?.type !== ValueTypes.REFINEE) { + return op; + } + + const type = refine.value.type; + if (type === AccessMethods.value) { + const args = refine.value.args.map((x) => resolveValue(structure, x)); + return op.value.apply(null, args); + } else if (type === AccessMethods.includes) { + const args = refine.value.args.map((x) => resolveValue(structure, x)); + return op.includes.apply(null, args); + } else if (type === AccessMethods.length) { + const args = refine.value.args.map((x) => resolveValue(structure, x)); + return op.length.apply(null, args); + } else { + throw new Error(`access method not recognized ${refine.value.type}`); + } +}; diff --git a/packages/engine.producer/src/graph/updateOperation.ts b/packages/engine.producer/src/graph/updateOperation.ts index 73b784e9..1327e458 100644 --- a/packages/engine.producer/src/graph/updateOperation.ts +++ b/packages/engine.producer/src/graph/updateOperation.ts @@ -7,7 +7,7 @@ import { EventNames, OperationTypes, UpdateValue, - UpdateValueMethods, + UpdateMethods, } from "@c11/engine.types"; import { randomId } from "@c11/engine.utils"; import isArray from "lodash/isArray"; @@ -24,7 +24,7 @@ export const updateOperation = ( const operationId = randomId(); //TODO: figure out how to infer the cb using typescript from // name - const wrapUpdate = (name: UpdateValueMethods, cb: any) => { + const wrapUpdate = (name: UpdateMethods, cb: any) => { return (...args: any[]) => { const patch = cb.apply(null, args); if (emit && patch) { @@ -40,7 +40,7 @@ export const updateOperation = ( }; const set = wrapUpdate( - UpdateValueMethods.SET, + UpdateMethods.set, (value: any, params: OperationParams) => { const path = getInvokablePath(structure, op, params); if (path) { @@ -57,7 +57,7 @@ export const updateOperation = ( ); const merge = wrapUpdate( - UpdateValueMethods.MERGE, + UpdateMethods.merge, (value: any, params: OperationParams) => { const path = getInvokablePath(structure, op, params); if (path) { @@ -83,24 +83,21 @@ export const updateOperation = ( } ); - const remove = wrapUpdate( - UpdateValueMethods.REMOVE, - (params: OperationParams) => { - const path = getInvokablePath(structure, op, params); - if (path) { - const patch = { - op: "remove", - path, - }; - db.patch([patch]); - return patch; - } - return; + const remove = wrapUpdate(UpdateMethods.remove, (params: OperationParams) => { + const path = getInvokablePath(structure, op, params); + if (path) { + const patch = { + op: "remove", + path, + }; + db.patch([patch]); + return patch; } - ); + return; + }); const push = wrapUpdate( - UpdateValueMethods.PUSH, + UpdateMethods.push, (value: any, params: OperationParams) => { const path = getInvokablePath(structure, op, params); if (path) { @@ -124,7 +121,7 @@ export const updateOperation = ( } ); - const pop = wrapUpdate(UpdateValueMethods.POP, (params: OperationParams) => { + const pop = wrapUpdate(UpdateMethods.pop, (params: OperationParams) => { const path = getInvokablePath(structure, op, params); if (path) { const val = db.get(path); @@ -145,11 +142,11 @@ export const updateOperation = ( }); const operation = { - [UpdateValueMethods.SET]: set, - [UpdateValueMethods.MERGE]: merge, - [UpdateValueMethods.REMOVE]: remove, - [UpdateValueMethods.PUSH]: push, - [UpdateValueMethods.POP]: pop, + [UpdateMethods.set]: set, + [UpdateMethods.merge]: merge, + [UpdateMethods.remove]: remove, + [UpdateMethods.push]: push, + [UpdateMethods.pop]: pop, __operation__: { id: operationId, symbol: UpdateOperationSymbol, diff --git a/packages/engine.types/src/producer.ts b/packages/engine.types/src/producer.ts index 946a09dc..d3f629b9 100644 --- a/packages/engine.types/src/producer.ts +++ b/packages/engine.types/src/producer.ts @@ -17,10 +17,11 @@ export enum OperationTypes { } export enum ValueTypes { - CONST = "CONST", - EXTERNAL = "EXTERNAL", - INTERNAL = "INTERNAL", - INVOKE = "INVOKE", + CONST = "CONST", // path member + EXTERNAL = "EXTERNAL", // prop + INTERNAL = "INTERNAL", // arg + INVOKE = "INVOKE", // param + REFINEE = "REFINEE", // last path member as invocation in header paths } export interface ConstValue { @@ -43,8 +44,18 @@ export interface InvokeValue { path: string[]; } +export type RefiningValueArg = ConstValue | ExternalValue | InternalValue; + +export interface RefiningValue { + type: ValueTypes.REFINEE; + value: { + type: AccessMethods | UpdateMethods; + args: RefiningValueArg[]; + }; +} + export type StaticValue = ConstValue | ExternalValue | InternalValue; -export type InvokableValue = StaticValue | InvokeValue; +export type InvokableValue = StaticValue | InvokeValue | RefiningValue; export type StaticPath = StaticValue[]; export type InvokablePath = InvokableValue[]; @@ -177,35 +188,55 @@ export type Params = { [k in keyof T]: string | number | Params; }; -export enum UpdateValueMethods { - SET = "set", - MERGE = "merge", - REMOVE = "remove", - PUSH = "push", - POP = "pop", +export enum UpdateMethods { + set = "set", + merge = "merge", + remove = "remove", + push = "push", + pop = "pop", } export type UpdateValue = { - [UpdateValueMethods.SET]: (value: T, params?: Params

) => void; - [UpdateValueMethods.MERGE]: (value: Partial, params?: Params

) => void; - [UpdateValueMethods.REMOVE]: (params?: Params

) => void; - [UpdateValueMethods.PUSH]: ( + [UpdateMethods.set]: (value: T, params?: Params

) => void; + [UpdateMethods.merge]: (value: Partial, params?: Params

) => void; + [UpdateMethods.remove]: (params?: Params

) => void; + [UpdateMethods.push]: ( value: T extends (infer R)[] ? R : unknown, params?: Params

) => void; - [UpdateValueMethods.POP]: (params?: Params

) => void; + [UpdateMethods.pop]: (params?: Params

) => void; }; -export enum GetValueMethods { - VALUE = "value", - INCLUDES = "includes", - LENGTH = "length", +//TODO: this should be the same as the Observe refining methods +// so that the syntax is compatible on both fronts +//TODO: Rename this operation as data access -> data update +// or shorter Access and Update +export enum AccessMethods { + value = "value", + includes = "includes", + length = "length", + // keys = "keys", + // isValid = "isValid", + // isEq = "isEq", // is equal + // isNe = "isNe", // is not equal + // isGt = "isGt", // is greater + // isLt = "isLt", // is less + // isGe = "isGe", // is greater or equal + // isLe = "isLe", // is less or equal + // and + // or + // not + // hasElement = "hasElement", // instead of includes for arrays + // hasChar = "hasChar" // + // hasProperty + // onDemand // update.foo.onDemand() - execution is conditioned by an observe + // next - act as a generator } export type GetValue = { - [GetValueMethods.VALUE]: (params?: Params

) => T; - [GetValueMethods.INCLUDES]: (value: any, params?: Params

) => boolean; - [GetValueMethods.LENGTH]: (params?: Params

) => number; + [AccessMethods.value]: (params?: Params

) => T; + [AccessMethods.includes]: (value: any, params?: Params

) => boolean; + [AccessMethods.length]: (params?: Params

) => number; }; export type ObservePath = T; diff --git a/packages/engine.utils/src/now.ts b/packages/engine.utils/src/now.ts index 53b281b7..92cf1bd2 100644 --- a/packages/engine.utils/src/now.ts +++ b/packages/engine.utils/src/now.ts @@ -1,6 +1,10 @@ // Used only when there is no micro/nano second precision const ms: { [k: number]: number } = {}; +//TODO: this should always be unique +// performance.now() might differ from implementation to implementation +// and we need a way to ensure that a timestamp is never given twice +// in the same browser session export const now = (): number => { if ( typeof performance !== "undefined" && From 40bc9825ff1a5ac8ad2b1bd8dd280a96673d1c36 Mon Sep 17 00:00:00 2001 From: Constantin Dumitrescu Date: Thu, 20 Oct 2022 12:57:44 +0300 Subject: [PATCH 02/19] docs: update get page with eager/lazy concept --- docs/docs/api/get.md | 70 +++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/docs/docs/api/get.md b/docs/docs/api/get.md index 28bc9ffd..bad1e050 100644 --- a/docs/docs/api/get.md +++ b/docs/docs/api/get.md @@ -4,26 +4,29 @@ title: get sidebar_label: get --- -## Overview +`get.` retrieve values from the state without adding a listener. -`get` provides the ability to get values from the global state at a later time, -after the `view` or `producer` was triggered. It works the same way as -[observe](/docs/api/observe), except: +It should be used for data that is required during execution but that does not trigger the execution (as opposed to [observe](/docs/api/observe)). -1. `get` don't provide a value, but instead an api for that path which can be used at - any time in future to get the latest value form state -2. `get` don't cause `view`s and `producer`s to get triggered +A `get` can be **lazy** or **eager** depending on whether the value should be available at the moment when the execution starts or at a later point in time. -`get` is ideal when: - -1. A value is needed to do a computation in a `producer`, but the producer - should not get triggered when this value changes -2. A value is needed at a later time since producer was triggered, e.g while - performing an asynchronous operation +A **lazy** `get` will return an API through which values can be retrieved and an **eager** `get` will provide the values when the execution starts. +Example: +```ts +const doSomeWork: producer = ({ + eager = get.foo.bar.value(), + lazy = get.foo.bar +}) => { + eager; // [1, 2, 3] + setTimeout(() => { + lazy.value(); // [1, 2, 3] + }, 1000); +} +``` ## API -`get.` returns an object with following properties: +A **lazy** `get.` returns an object with following properties: 1. `.value(params?: object)` returns the date stored at that `` `params` is an optional object argument, the keys of which set the @@ -38,27 +41,28 @@ Javascript type, a plain object), a copy of the data is returned. However, if the data is not serializable (e.g a class instance, function etc), a reference to it is returned. -## Example +## Best practices -For example, if the state looks like: +1. Use more `gets` than `observes` in producers. -```json -{ - "foo": { - "bar": "baz" - } -} -``` + Producers should have a very narrow window for triggering and the `observe` should be reserved for the triggering specificity. When you can't justify that some data determines the triggering then that data should be retrieved using a `get`. -The value of `bar` can be accessed by assigning `get.foo.bar` in header of a -[view](/docs/api/view) or [producer](/docs/api/producer), and calling it later -e.g +2. `views` should not use `get`. -``` -const doSomeWork: producer = ({ getBar = get.foo.bar }) => { - const var = getBar.value(); // provides lates value of bar, and does not trigger if bar ever changes -} -``` + Views should be completely reactive and always rely on fresh data to re-render. + + The only valid exception is regarding handling a user event (like a `click`) + which is outside rendering. + ```ts + // DON'T DO THIS! + const a: view = ({ a = get.a }) =>

{a.value()}
+ + // DON"T DO THIS! + const b: view = ({ a = get.a.value() }) =>
{a}
+ + // ONLY VALID USE CASE + const c: view = ({ a = get.a }) =>