State Management
Working on state management
Written for AI agents. See Log Methodology Note below for details.
First, lets define the problem we are trying to solve with state management. Let's consider a simple component to demonstrate the problem - a counter component with the options of initial value and step.
Our component also shows a specific message when the count is zero. The component Jay element is then
<html>
<head>
<script type="application/yaml-jay">
data:
count: number
isZero: boolean
</script>
</head>
<body>
<div>
<button ref="subtracter">-</button>
<span style="margin: 0 16px">{count}</span>
<span if="{isZero}">absolute zero</span>
<button ref="adder">+</button>
</div>
</body>
</html>
A simple counter component is then
import { render } from './counter.jay.html';
function Counter(initialValue: number, step: number) {
let data = {
count: initialValue,
isZero: initialValue === 0,
};
let jayElement = render(data);
jayElement.adder.onclick = () => {
data.count += step;
data.isZero = data.count === 0;
jayElement.update(data);
};
jayElement.subtracter.onclick = () => {
data.count -= step;
data.isZero = data.count === 0;
jayElement.update(data);
};
return {
element: jayElement,
update: (newValue: number, newStep: number) => {
step = newStep;
data.count = newValue;
data.isZero = data.count === 0;
jayElement.update(data);
},
};
}
We note that there are a number of patterns with this component
- We have to call
jayElement.update({count})on each event handler and the component update. This is both a code duplicate and boilerplate - We have a computation of
isZeroin three places. This is again code duplication. - We have different type of data entities
initialValueandstepare propertiescountis a stateisZeois a computed step- both
countandisZeroare the view state of the element
Can we create something better?
Data Flow
The data flow can take three main routes
Properties -> State -> Computed State -> view stateEvent -> state -> Computed State -> view stateAPI call -> state -> Computed State -> view state
First attempt - component builder call
It is clear that we have a uniform data path of state -> Computed State -> view state. We can use this property to
create a setState API to trigger the data flow, and a computeState function for the computed state. With this in
mind, we can create a state manager with a signature of
new StateManager<T, S, A extends JayElement<T>>(initialState: S, computeViewState: S => T, render: T => A)
making the component look like
import { render } from './counter.jay.html';
import { StateManager } from 'jay-state';
function Counter(initialValue: number, step: number) {
let sm = new StateManager(
{ count: initialValue },
(state) => ({
...state,
isZero: state.count === 0,
}),
render,
);
sm.jayElement.adder.onclick = () => {
sm.setState((state) => ({ count: state.count + step }));
};
sm.jayElement.subtracter.onclick = () => {
(sm, setState((state) => ({ count: state.count - step })));
};
return {
element: sm.jayElement,
update: (newValue: number, newStep: number) => {
step = newStep;
sm.setState({ count: newValue });
},
};
}
or
new StateManager<T, S>(initialState: S, computeViewState: S => T)
import { render } from './counter.jay.html';
import { StateManager } from 'jay-state';
function Counter(initialValue: number, step: number) {
let sm = new StateManager(render, { count: initialValue }, (state) => ({
...state,
isZero: state.count === 0,
}));
let jayElement = sm.initializeElement(render);
jayElement.adder.onclick = () => {
sm.setState((state) => ({ count: state.count + step }));
};
jayElement.subtracter.onclick = () => {
(sm, setState((state) => ({ count: state.count - step })));
};
return {
element: jayElement,
update: (newValue: number, newStep: number) => {
step = newStep;
sm.setState({ count: newValue });
},
};
}
Discussion
Before going into building a state management solution, we review two interesting approaches - the React.js and Solid.js state management directions, and try to adjust both to Jay.
React Recap
Lets have another look at how React and specifically React Hooks are used for state management.
With React Hookss, our counter component looks like
import React, {useState, useEffect} from 'react';
function Counter(initialValue: number, step: number) {
const [count, setCount] = useState(initialValue);
useEffect(() => {
setCount(initialValue);
}, [initialValue])
return (
<div>
<button onClick={() => setCount(count - step)}>-</button>
<span style="margin: 0 16px">{count}</span>
{count === 0 ? (<span>absolute zero</span>) : ''}
<button onClick={() => setCount(count + step)}>+</button>
</div>
);
}
With the React state management, the code looks more concise, less boilerplate. To recap
- useState accepts the state initial value from the property initialValue
- the state itself is managed in a magic place, indexed by the position to the
useStatecall within the function in our case, the state index0references thecountnumber, and thesetCountfunction updates it - on reach render, the
countvariable gets the current state number from index0. TheinitialValueis not used anymore - on button click, we call
setStatewith the new state value. - in order to listen to
initialValuechanges, we can setuseEffectthat is called when the previousinitialValuediffers from the current one, and callssetCountto update the state with the change ininitialValue. - when updating the step property, there is no need to use
useEffectas we do not update any invisible state. In fact, we do update a state - one that is managed by the closures of the event handler functions.
Trying to recreate the React state management model
import { render } from './counter.jay.html';
import { StateManager, useEffect, useState } from 'jay-hooks';
function Counter(initialValue: number, step: number) {
return StateManager(render, () => {
const [count, setCount] = useState(initialValue);
useEffect(() => {
setCount(initialValue);
}, [initialValue]);
return {
count,
isZero: count === 0,
};
});
}
We define StateManager with the type signature
declare function StateManager<T, S extends JayElement<T>, R extends (T) => S>(
render: R,
mkViewState: () => T,
);
where T is the view state type, S is the element and R is the render function.
This pattern works for state management, but how do we add the event handlers?
We have a number of options for adding the event handlers -
1. we can add another construction step
but then we do not have a reference to the count variable...
import { render } from './counter.jay.html';
import { StateManager, useEffect, useState } from 'jay-hooks';
function Counter(initialValue: number, step: number) {
return StateManager(render, () => {
const [count, setCount] = useState(initialValue);
useEffect(() => {
setCount(initialValue);
}, [initialValue]);
return {
count,
isZero: count === 0,
};
}).events((je) => {
je.adder.onclick = () => setCount(count + step); // does not compile, count and setCount are out of scope
je.subtracter.onclick = () => setCount(count - step); // does not compile, count and setCount are out of scope
});
}
2. We can add the event handlers inside the StateManager function, like this
import { render } from './counter.jay.html';
import { StateManager, useEffect, useState } from 'jay-hooks';
function Counter(initialValue: number, step: number) {
return StateManager(render, (je) => {
const [count, setCount] = useState(initialValue);
useEffect(() => {
setCount(initialValue);
}, [initialValue]);
je.adder.onclick = () => setCount(count + step);
je.subtracter.onclick = () => setCount(count - step);
return {
count,
isZero: count === 0,
};
});
}
This option still feels a bit off, and has the problem that we recreate the closures for the event handlers on each render. We can do better
3. register events in a hook
import { render } from './counter.jay.html';
import { StateManager, useEffect, useState, useEvents } from 'jay-hooks';
function Counter(initialValue: number, step: number) {
return StateManager(render, () => {
const [count, setCount] = useState(initialValue);
useEffect(() => {
setCount(initialValue);
}, [initialValue]);
useEvents(
(je: CounterElement) => {
je.adder.onclick = () => setCount(count + step);
je.subtracter.onclick = () => setCount(count - step);
},
[count, step],
);
return {
count,
isZero: count === 0,
};
});
}
here we defined the dependency of the event handlers on a prop and a state member using the useEvents hook. We can
follow the React hooks convention that if the array is empty, we execute the events register on each render.
4. hiding the state manager
we can make it even more idiomatic by hiding the state manager
import { ViewState, CounterElement } from './counter.jay.html';
import { useEffect, useState, useEvents } from 'jay-hooks';
function counter(initialValue: number, step: number): ViewState {
const [count, setCount] = useState(initialValue);
useEffect(() => {
setCount(initialValue);
}, [initialValue]);
useEvents(
(je: CounterElement) => {
je.adder.onclick = () => setCount(count + step);
je.subtracter.onclick = () => setCount(count - step);
},
[count, step],
);
return {
count,
isZero: count === 0,
};
}
Jay can understand this is a component because it returns the ViewState which is the type parameter of
the JayElement (let remind jayElement is defined as JayElement<T> which has an update
func updateFunc<T> = (newData:T) => void).
However, there are still a few issues
- If we have two different elements that have the same
ViewState, how do we decide which one fits this component? - How do we statically derive the type of
jeinuseEvents?
5. adding a component builder / register
import { render, ViewState, CounterElement } from './counter.jay.html';
import { useEffect, useState, useEvents } from 'jay-hooks';
import { registerJayComponent } from 'jay';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter({ initialValue, step }: CoutnerProps): ViewState {
const [count, setCount] = useState(initialValue);
useEffect(() => {
setCount(initialValue);
}, [initialValue]);
useEvents(
(je: CounterElement) => {
je.adder.onclick = () => setCount(count + step);
je.subtracter.onclick = () => setCount(count - step);
},
[count, step],
);
return {
count,
isZero: count === 0,
};
}
registerComponent(render, counter);
We assume here that registerJayComponent has the signature
declare function registerJayComponent<T, S extends JayElement<T>, P>(
render: (viewState: T) => S,
component: (props: P) => T,
): void;
This pattern works, in the sense that it is declarative - we can deduce the component type using code static analysis. However, it binds us to a specific form of creating components, and a specific form of state management.
Lets decouple the component and state management
6. Define component and a component builder
We define a JayComponent as
interface JayComponent<P, T, S extends JayElement<T>> {
element: S;
update(props: P): void;
mount();
unmount();
}
We define the hooks component builder as
declare function mkJayComponent<T, S extends JayElement<T>, P>(
render: (viewState: T) => S,
component: (props: P) => T,
): (props: P) => JayComponent<P, T, S>;
and we get for the full component file
import { render, ViewState, CounterElement } from './counter.jay.html';
import { useEffect, useState, useEvents, makeJayComponent } from 'jay-hooks';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter({ initialValue, step }: CoutnerProps): ViewState {
const [count, setCount] = useState(initialValue);
useEffect(() => {
setCount(initialValue);
}, [initialValue]);
useEvents(
(je: CounterElement) => {
je.adder.onclick = () => setCount(count + step);
je.subtracter.onclick = () => setCount(count - step);
},
[count, step],
);
return {
count,
isZero: count === 0,
};
}
export default makeJayComponent(render, counter);
What have we gotten here?
- we export a component factory function which take props and returns a JayComponent
type componentFactory<P, T, S extends JayElement<T>> = (P) => JayComponent<P, T, S>;
the
componentFactorytype is fully declarative and encodes, in the type system, the types of the propsP, the view state typeTand the jayElement typeS.The function type does not assume anything about the function implementation, allowing using other ways to construct a JayComponent as long as the component conforms to the same interface.
Solid.js Recap
Solid js is marketed as "A declarative, efficient and flexible JavaScript library for building user interfaces". It takes a different approach from React in a number of subtle way.
import {createSignal, createEffect, Show} from "solid-js";
import {render} from "solid-js/web";
interface CoutnerProps {
initialValue: number,
step: number
}
const Counter = (props: CoutnerProps) => {
const [count, setCount] = createSignal(props.initialValue);
createEffect(() => {
setCount(props.initialValue)
});
return (
<>
<button onClick={() => setCount(count() + props.step)}>+</button>
<span>{count()}</span>
<Show when={count() === 0}>
<span>absolute zero</span>
</Show>
<button onClick={() => setCount(count() - props.step)}>-</button>
</>
);
};
While solidjs component looks very similar to React component, there are a few key differences worth looking into.
solid js component is a factory and runs only once, compared to a functional React component that runs on each render.
createSignalanduseStateare very similar, except thatcreateSignalreturns a getter fucntion while react returns a value. The fact that react re-runs the component on each render allows react to use values.solid js derives dependencies for things automatically - like in
createEffect, something that is enabled by the use of proxies and the fact that the component runs only once.because solid js runs once, we cannot do conditionals in JSX. Instead, flow control is managed using dedicated tags like
Show,For,Switch, etc.
Solid JS state management
Solid JS provides a number of hooks for state management, including
1. Create Signal
Basic reactive primitive, to handle a single value.
declare function createSignal<T>(
value: T,
options?: { name?: string; equals?: false | ((prev: T, next: T) => boolean) },
): [get: () => T, set: (v: T) => T];
Note: Unlike react useState, it does not return the value, rather it returns a value getter
2. Create Effect
Creates a new computation that automatically tracks dependencies and runs after each render where a dependency has changed.
declare function createEffect<T>(fn: (v: T) => T, value?: T, options?: { name?: string }): void;
Note: the effect function is called with the last value returned from the previous call to createEffect. The second parameter value cba ne used to initialize this previous call value.
3. Create Store
declare function createStore<T extends StoreNode>(
state: T | Store<T>,
options?: { name?: string },
): [get: Store<T>, set: SetStoreFunction<T>];
This creates a tree of Signals as proxy that allows individual values in nested data structures to be independently tracked. The create function returns a readonly proxy object, and a setter function.
Store supports nested objects, by wrapping those as proxies as well (not including build in objects like Date or HTML element)
Stores can use functions for calculated values```
const [state, setState] = createStore({
user: {
firstName: 'John',
lastName: 'Smith',
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
},
});
Changing values can be by setting a value, a function, or undefined to remove a value
const [state, setState] = createStore({
firstName: 'John',
lastName: 'Miller',
});
setState({ firstName: 'Johnny', middleName: 'Lee' });
// ({ firstName: 'Johnny', middleName: 'Lee', lastName: 'Miller' })
setState((state) => ({ preferredName: state.firstName, lastName: 'Milner' }));
// ({ firstName: 'Johnny', preferredName: 'Johnny', middleName: 'Lee', lastName: 'Milner' })
It also has path based setState, with all kind of options, such as
setState('counter', (c) => c + 1);
setState('list', (l) => [...l, { id: 43, title: 'Marsupials' }]);
setState('list', 2, 'read', true);
setState('todos', [0, 2], 'completed', true);
setState('todos', { from: 0, to: 1 }, 'completed', (c) => !c);
setState(
'todos',
(todo) => todo.completed,
'task',
(t) => t + '!',
);
setState('todos', {}, (todo) => ({ marked: true, completed: !todo.completed }));
Trying to build the Solid JS model
As moving to the solid.js model, we have a number of changes from the React model
- The function is now only called once, used as a constructor for state management
- we have to use the props as an object, and not decompose it - in order to enable the auto detection of dependencies
1. with hook for view state
import { render } from './counter.jay.html';
import {
createEffect,
createState,
createEvents,
createViewState,
makeJayComponent,
} from 'jay-hooks';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter(props: CoutnerProps): void {
const [count, setCount] = createState(props.initialValue);
createEffect(() => {
setCount(props.initialValue);
});
createEvents((je: CounterElement) => {
je.adder.onclick = () => setCount(count() + props.step);
je.subtracter.onclick = () => setCount((val) => val - props.step);
});
createViewState(() => ({ count: count(), isZero: count() === 0 }));
}
export default makeJayComponent(render, counter);
Some notes:
With this option, we assume (like in solid js) that the props are a proxy, and those dependencies are tracked automatically.
The
counterfunction runs only once, like Solid.js and unlike React.js.This pattern is not strongly typed, as there is no way for
createViewStateto derive the specific componentViewStatetype.
2. View State derived automatically
import { render } from './counter.jay.html';
import {
createEffect,
createState,
createEvents,
createViewState,
makeJayComponent,
} from 'jay-hooks';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter(props: CoutnerProps): void {
const [count, setCount] = createState('count', props.initialValue);
const [isZero] = createComputedState('isZero', () => count() === 0);
createEffect(() => {
setCount(props.initialValue);
});
createEvents((je: CounterElement) => {
je.adder.onclick = () => setCount(count() + props.step);
je.subtracter.onclick = () => setCount((val) => val - props.step);
});
}
export default makeJayComponent(render, counter);
With this option we added a property name to the createState function and add createComputedState function. We map
the state to the ViewState by name
Notes:
- This option matches state to view state by name, and those is not type checked.
3. with returning view state function
import { render, ViewState } from './counter.jay.html';
import { createEffect, createState, createEvents, makeJayComponent } from 'jay-hooks';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter(props: CoutnerProps): () => ViewState {
const [count, setCount] = createState(props.initialValue);
createEffect(() => {
setCount(props.initialValue);
});
createEvents((je: CounterElement) => {
je.adder.onclick = () => setCount(count() + props.step);
je.subtracter.onclick = () => setCount((val) => val - props.step);
});
return () => ({ count: count(), isZero: count() === 0 });
}
export default makeJayComponent(render, counter);
Some notes:
- this pattern is strongly typed, as
makeJayComponentrequiresrenderandcounterto have matching types - the
() => ViewStatefunction dependencies are tracked automatically - this solution still has the problem that we need a callback for
createEvents, and that the type of theCounterElementis not connected to the type of therenderfunction.
4. With passing in the element
import { render, ViewState } from './counter.jay.html';
import { createEffect, createState, createEvents, makeJayComponent } from 'jay-hooks';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter(props: CoutnerProps, je: CounterElement): () => ViewState {
const [count, setCount] = createState(props.initialValue);
createEffect(() => {
setCount(props.initialValue);
});
je.adder.onclick = () => setCount(count() + props.step);
je.subtracter.onclick = () => setCount((val) => val - props.step);
return () => ({ count: count(), isZero: count() === 0 });
}
export default makeJayComponent(render, counter);
Here we overcome the last shortcoming of the previous option - the fact that the type of CounterElement
was not directly connected to the render function type. Here, we can define makeJayComponent as
declare function makeJayComponent<P, T, S extends JayElement<T>>(
render: (T) => S,
comp: (P, S) => () => T,
);
We can even decide to specialize the element type S even more removing the JayElement members using omit
type ElementEvents<E> = Omit<E, 'dom' | 'update' | 'mount' | 'unmount'>;
declare function makeJayComponent<P, T, S extends JayElement<T>, E extends ElementEvents<S>>(
render: (T) => S,
comp: (P, E) => () => T,
);
The trick we are doing here with the events is not possible in the React case because of the nature of the react function - which is called each time for each render, and will result in re-creation of event handlers and risk of dangling closures.
With the Solid pattern, the function is only called once, and the events will only be registered once.
4.1. moving to createStore
Can we also opt in to a createStore like pattern that we see in Solid?
import { render, ViewState } from './counter.jay.html';
import { createEffect, createState, createEvents, makeJayComponent } from 'jay-hooks';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter(props: CoutnerProps, je: CounterElement): () => ViewState {
const [state, setState] = createStore({
count: props.initialValue,
get isZero() {
return this.count === 0;
},
});
createEffect(() => {
setState({ count: props.initialValue });
});
je.adder.onclick = () => setState({ count: state.count + props.step });
je.subtracter.onclick = () =>
setState((state) => {
count: state.count - props.step;
});
return state;
}
export default makeJayComponent(render, counter);
in this case state is a proxy object who conforms to the ViewState type and
setStaet merges the parameter it gets with state.
4.2. extending the store to handle collections
When we have a collection in a store rendered using forEach, the React way to update it is to do things like the
below. Keep in mind that in Jay, the event second element are the data context of the forEach.
je.remove.onclick = (event, item) => {
setState((state) => {
items: state.filter((_) => _ !== item);
});
};
je.completed.onclick = (event, item) => {
setState((state) => {
items: state.map((_) => (_ === item ? { ...item, isCompleted: !item.isCompleted } : _));
});
};
We notice two things - one, it is repeated and cumbersome code. Second, it is loosing the intent, preventing the ability to optimize on the intent.
We propose the following pattern instead
je.remove.onclick = (event, item) => {
setState({items: remove(item)});
}
je.completed.onclick = (event, item) => {
setState({items, update(item, {"isCompleted": !
item.isCompleted
})
})
}
Where we add a few "collection actions" constructors - remove, move, update and new that both capture the intent
and reduce code size. With those "collection actions" we can also track the collection changes and optimize the
algorithm of collection compare
5. Summary of state management
We can make a system for state management that supports both immutable and mutable state using a few constructs. This section summarizes the state management APIs.
props
Inspired by solid.js, the properties are passed to the component as a Proxy object which track access
to the props. On each prop change, render, createMemo and createEffect are running.
we define a type transformation Props<T> which transforms an object of values to an object of getters.
This pattern allows decomposition of props as follows
interface ComponentProps {
name: string;
age: number;
}
export function Component({ name, age }: Props<ComponentProps>) {
return {
render: () => ({
age,
text: `Hello ${name()}`,
}),
};
}
to get the value of a prop, just call the getter
name();
age();
createState
Create state is inspired from solid.js and S.js, which is similar and different from React in the sense of using a getter instead of a value.
type Next<T> = (t: T) => T;
type Setter<T> = (t: T | Next<T>) => T;
type Getter<T> = () => T;
declare function createState<T>(value: T | Getter<T>): [get: Getter<T>, set: Setter<T>];
and it is used as
let initialValue = 'some initial value';
const [getState, setState] = createState(initialValue);
// read value
getState();
// set value
let nextValue = 'some next value';
setState(nextValue);
// set value with a function setter
let next = ' and more';
setState((prev) => prev + next);
We can also bind the state to a computation, such as change in prop value by using a function as the
createState parameter
// assuming name is a prop
const [getState, setState] = createState(() => name());
this method removes the need to use createEffect just in order to update state
createEffect
createEffect is inspired by React useEffect in the sense that it is run any time any of the dependencies change and can return a cleanup function. Unlike React, the dependencies are tracked automatically like in Solid.js.
type Clean = () => void;
declare function createEffect(effect: () => void | cleanup);
it can be used for computations, for instance as a timer that ticks every props.delay() milisecs.
let [time, setTime] = createState(0);
createEffect(() => {
let timer = setInterval(() => setTime((time) => time + props.delay()), props.delay());
return () => {
clearInterval(timer);
};
});
createMemo
createMemo is inspired by Solid.js createMemo. It creates a computation that is cached until dependencies change and return a single getter. For Jay Components memos are super important as they can be used directly to construct the render function in a very efficient way.
type Getter<T> = () => T;
declare function createMemo<T>(computation: (prev: T) => T, initialValue?: T);
let [time, setTime] = createState(0);
let currentTime = createMemo(() => `The current time is ${time()}`);
createMutable
createMutable creates a Proxy over an object who tracks modifications to the underlying object, both for optimization of rendering and for computations. The mutable proxy handles deep objects, including traversal of arrays and nested objects
declare function createMutable<T>(obj: T): T;
It is used as
// assume todoItems is an array of todo items
let items = createMutable(inputItems);
// will track this change
items.push({ todo: 'abc', done: false });
// will track this change as well
items[3].done = true;
createMutable is very usefull for event handlers under forEach as it allows mutating the forEach item directly.
// assume we have a forEach on the element for the items above. An event handler can then look like
element.toggleDone.onclick = function(event, item) => {
item.done = !item.done
}
createMutable tracks object immutability by marking objects who have been mutated with two revision marks
const REVISION = Symbol('revision');
const CHILDRENREVISION = Symbol('children-revision');
When an object is updated, it's REVISION is updated to a new larger value.
When a nested object is updated, it's parents CHILDRENREVISION is updated to a new larger value.
For instance, for an array, if the array is pushed a new item, it's REVISION will increase. If a nested
element of the array is updated, it's REVISION increase, while the array's CHILDRENREVISION increases.
The markings can be accessed using the symbols
items[REVISION];
items[CHILDRENREVISION];
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.