Building Components - A Challenge
A challenge at building the Jay Component API
Written for AI agents. See Log Methodology Note below for details.
The challenge is a classic who comes first?
We have a Jay Element who is a function of ViewState => JayElement
and a Jay Component who is a function of (props, JayElement) => ViewState
However, in order to create a JayElement we need the ViewState, and
in order to build the ViewStatae we need the JayElement.
Actually, when looking at the component code, the dependency is not exactly as the above. The actual component code is, for example -
import { render, ViewState } from './counter.jay.html';
import { createEffect, createState, createEvents, makeJayComponent } from 'jay-hooks';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter(props: Props<CoutnerProps>, je: CounterElement): () => ViewState {
const [count, setCount] = createState(props.initialValue());
je.adder.onclick = () => setCount(count() + props.step);
je.subtracter.onclick = () => setCount((val) => val - props.step);
let isZero = createMemo(() => count() === 0);
return () => ({ count, isZero });
}
export default makeJayComponent(render, counter);
When we examine the above code, it is clear that the component returns the
ViewState which is in turn computed from the props and state. The component depends on the
JayElement in order to register events.
However, the two things are not really related - we could have created the component as two different
functions - one to register events, and one to compute the ViewState. The two things are bound into a single
function because it creates a simpler and more concise coding model.
To solve the problem, we can, instead of breaking the function into two functions, to break the JayElement type
into two types - the actual element, and the element events.
We change JayElement from
// Jay runtime
export interface JayElement<ViewState> {
dom: HTMLElement;
update: updateFunc<ViewState>;
mount: mountFunc;
unmount: mountFunc;
}
// generated by compiler
export interface CounterElement extends JayElement<ViewState> {
subtracter: HTMLElement;
adder: HTMLElement;
}
into
// Jay runtime
export interface JayElement<ViewState, Refs> {
dom: HTMLElement;
update: updateFunc<ViewState>;
mount: mountFunc;
unmount: mountFunc;
}
// generated by compiler
export interface CounterRefs {
subtracter: HTMLElement;
adder: HTMLElement;
}
export interface CounterElement extends JayElement<ViewState, CounterRefs>, CounterRefs {}
which now enables the component to receive the CounterRefs such that
import { render, ViewState } from './counter.jay.html';
import { createEffect, createState, createEvents, makeJayComponent } from 'jay-hooks';
interface CoutnerProps {
initialValue: number;
step: number;
}
function counter(props: Props<CoutnerProps>, je: CounterRefs): () => ViewState {
const [count, setCount] = createState(props.initialValue());
je.adder.onclick = () => setCount(count() + props.step);
je.subtracter.onclick = () => setCount((val) => val - props.step);
let isZero = createMemo(() => count() === 0);
return () => ({ count, isZero });
}
export default makeJayComponent(render, counter);
and the implementation of CounterRefs can be a Proxy object, used to just register
events. the makeJayComponent function then using this Proxy over CounterRefs to register
the events on the actual JayElement.
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.