code-docs/architecture/state-management.md
How Lowdefy manages page and application state.
Lowdefy state management is:
| Operator | Source | Scope | Mutable |
|---|---|---|---|
_state | context.state | Page | Yes |
_input | lowdefy.inputs[contextId] | Page | Read-only |
_global | lowdefy.lowdefyGlobal | App | Read-only |
_url_query | window.location.search | Browser | External |
File: packages/engine/src/State.js
class State {
constructor(context) {
this.context = context;
this.frozenState = null;
this.initialized = false;
}
set(field, value) {
// Dot-notation path support
set(this.context.state, field, value);
}
del(field) {
// Remove field and clean empty parents
unset(this.context.state, field);
}
freezeState() {
// Called after onInit - snapshot for reset
if (!this.initialized) {
this.frozenState = serializeToString(this.context.state);
this.initialized = true;
}
}
resetState() {
// Restore to frozen initial state
Object.keys(this.context.state).forEach((key) => delete this.context.state[key]);
const frozen = deserializeFromString(this.frozenState);
Object.keys(frozen).forEach((key) => this.set(key, frozen[key]));
}
swapItems(field, from, to) {
// Swap array items (for reordering)
}
removeItem(field, index) {
// Remove array item at index
}
}
File: packages/engine/src/getContext.js
const ctx = {
id,
pageId: config.pageId,
eventLog: [],
jsMap,
requests: {},
state: {}, // Empty state object
_internal: {
lowdefy,
rootBlock: config,
parser: WebParser,
State: new State(ctx),
Actions: new Actions(ctx),
Requests: new Requests(ctx),
RootAreas: new Areas({...}),
update: () => _internal.RootAreas.update()
}
};
getContext()
↓
RootAreas.init()
↓
Block.reset(initState)
↓
Input blocks: State.set(blockId, initValue)
↓
Block.evaluate()
↓
onInit event
↓
State.freezeState() // Snapshot for reset
File: packages/plugins/operators/operators-js/src/operators/shared/state.js
function _state({ arrayIndices, location, params, state }) {
return getFromObject({
arrayIndices,
location,
object: state,
operator: '_state',
params,
});
}
Usage:
# Simple access
value:
_state: user.name
# With default
value:
_state:
key: user.name
default: "Anonymous"
File: packages/plugins/operators/operators-js/src/operators/client/input.js
function _input({ arrayIndices, input, location, params }) {
return getFromObject({
arrayIndices,
location,
object: input,
operator: '_input',
params,
});
}
File: packages/plugins/operators/operators-js/src/operators/client/global.js
function _global({ arrayIndices, location, lowdefyGlobal, params }) {
return getFromObject({
arrayIndices,
location,
object: lowdefyGlobal,
operator: '_global',
params,
});
}
File: packages/plugins/operators/operators-js/src/operators/client/url_query.js
function _url_query({ arrayIndices, globals, location, params }) {
const { window } = globals;
return getFromObject({
arrayIndices,
location,
object: urlQuery.parse(window.location.search.slice(1)),
operator: '_url_query',
params,
});
}
Usage:
# URL: ?user=john&id=123
userId:
_url_query: id # Returns "123"
File: packages/engine/src/Block.js
_initInput = () => {
this.setValue = (value) => {
// Type enforcement
this.value = type.enforceType(this.meta.valueType, value);
// Store in state
this.context._internal.State.set(this.blockId, this.value);
// Mark for re-render
this.update = true;
this.context._internal.update();
};
};
User types in input
↓
Block component calls block.setValue(newValue)
↓
Type enforced via block metadata
↓
State.set(blockId, value)
↓
block.update = true
↓
context._internal.update()
↓
Areas.updateStateFromRoot()
↓
Block.evaluate() for all blocks
↓
React re-render
File: packages/engine/src/Areas.js
update = () => {
this.updateStateFromRoot();
this.renderBlocks();
};
updateStateFromRoot = () => {
const repeat = this.recEval(true);
this.updateState();
// Re-evaluate if visibility changed (max 20 iterations)
if (repeat && this.recCount < 20) {
this.recCount += 1;
this.updateStateFromRoot();
}
};
File: packages/engine/src/Block.js
evaluate = (visibleParent, repeat) => {
// Sync state value for input blocks
if (this.isInput()) {
const stateValue = get(this.context.state, this.blockId);
this.value = type.isUndefined(stateValue) ? this.value : stateValue;
}
// Evaluate all expressions
this.propertiesEval = this.parse(this.properties);
this.styleEval = this.parse(this.style);
this.visibleEval = this.parse(this.visible);
// Mark for render if changed
if (this.before !== after) {
this.update = true;
}
};
File: packages/engine/src/actions/createSetState.js
function createSetState({ arrayIndices, context }) {
return function setState(params) {
Object.keys(params).forEach((key) => {
context._internal.State.set(applyArrayIndices(arrayIndices, key), params[key]);
});
context._internal.RootAreas.reset();
context._internal.update();
};
}
Usage:
events:
onClick:
- id: setUser
type: SetState
params:
user:
name: John
email: [email protected]
File: packages/engine/src/actions/createReset.js
function createReset({ context }) {
return function reset() {
context._internal.State.resetState();
context._internal.RootAreas.reset(deserializeFromString(context._internal.State.frozenState));
};
}
File: packages/engine/src/Block.js
this.pushItem = () => {
this.subAreas.push(
this.newAreas({ arrayIndices: [...], initState: {} })
);
this.update = true;
this.context._internal.update();
};
this.removeItem = (index) => {
this.context._internal.State.removeItem(this.blockId, index);
this.subAreas.splice(index, 1);
// Re-index remaining items
this.update = true;
this.context._internal.update();
};
File: packages/engine/src/Requests.js
async callRequest({ actions, arrayIndices, blockId, event, requestId }) {
const requestConfig = this.requestConfig[requestId];
// Parse payload - resolves all operators including _state
const { output: payload } = this.context._internal.parser.parse({
actions,
event,
arrayIndices,
input: requestConfig.payload,
location: requestId,
});
return this.fetch({ payload, requestId, ... });
}
Example:
requests:
- id: saveUser
type: MongoDBUpdateOne
connectionId: mongodb
payload:
filter:
_id:
_state: selectedUserId
update:
$set:
name:
_state: form.name
email:
_state: form.email
File: packages/client/src/block/Block.js
const Block = ({ block, Blocks, context, lowdefy, parentLoading }) => {
const [updates, setUpdate] = useState(0);
// Register updater in lowdefy context
lowdefy._internal.updaters[block.id] = () => setUpdate(updates + 1);
// Re-render when state changes trigger update
return <CategorySwitch ... />;
};
State.set() called
↓
context._internal.update()
↓
Areas.updateStateFromRoot()
↓
Block.evaluate() computes new values
↓
lowdefy._internal.updateBlock(blockId)
↓
lowdefy._internal.updaters[blockId]()
↓
React useState triggers re-render
File: packages/utils/helpers/src/get.js
get(object, 'user.profile.name', { default: null, copy: true });
// Supports:
// - Dot notation: 'a.b.c'
// - Array indices: 'items.0.name'
// - Default values
// - Deep copy option
File: packages/utils/helpers/src/set.js
set(state, 'user.profile.name', 'John');
// Features:
// - Auto-creates intermediate objects
// - Handles array index paths
// - Prevents prototype pollution
File: packages/utils/helpers/src/type.js
type.enforceType('string', value);
type.enforceType('array', value);
type.enforceType('object', value);
Scenario: User enters name, triggers request
User Input
text_input_1setValue Called
block.setValue('John')State Updated
State.set('text_input_1', 'John')context.state['text_input_1'] = 'John'Update Triggered
context._internal.update()Evaluation
{
name: {
_state: 'text_input_1';
}
}
// Becomes: { name: 'John' }
Request Made
Response
context.requests[requestId]Display
_request operatorWhen blocks become invisible:
| Component | File |
|---|---|
| State Class | packages/engine/src/State.js |
| Context Factory | packages/engine/src/getContext.js |
| Block Engine | packages/engine/src/Block.js |
| Areas Manager | packages/engine/src/Areas.js |
| SetState Action | packages/engine/src/actions/createSetState.js |
| Reset Action | packages/engine/src/actions/createReset.js |
| _state Operator | packages/plugins/operators/operators-js/src/operators/shared/state.js |
| _input Operator | packages/plugins/operators/operators-js/src/operators/client/input.js |
| _global Operator | packages/plugins/operators/operators-js/src/operators/client/global.js |
| _url_query Operator | packages/plugins/operators/operators-js/src/operators/client/url_query.js |
| React Block | packages/client/src/block/Block.js |
context.state with dot-notation keyssetValue()