Lifecycle
Lifecycle demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.
App.tsx
import './App.css';
import Button from './common/Button';
import { Component, get, has, ref } from '@expressive/mvc';
export default () => (
<div className="container">
<h1>Lifecycle</h1>
<p>
Three seams, three phases. <code>new()</code> runs at construction,
synchronously and during server render too, so it belongs to setup the
instance carries with it. An effect declared there waits for commit and
runs only on the client - where timers, listeners and anything reaching
for <code>window</code> go. <code>ref()</code> fires when its element
attaches.
</p>
<Demo />
<small>
Toggle the probe to watch each seam fire, then unwind in reverse. The
transcript lives on the demo, which outlives the probe writing to it.
</small>
</div>
);
class Demo extends Component {
showing = true;
entries = has<string>();
log(note: string) {
this.entries.push(note);
}
render() {
const { showing, entries } = this;
return (
<>
{showing && <Probe />}
<div className="row">
<Button primary onClick={() => (this.showing = !showing)}>
{showing ? 'Unmount' : 'Mount'}
</Button>
<Button onClick={() => entries.clear()}>Clear</Button>
</div>
<ol className="trace">
{entries.map((note, i) => (
<li key={i}>{note}</li>
))}
</ol>
</>
);
}
}
class Probe extends Component {
demo = get(Demo);
width = 0;
ticks = 0;
protected new() {
this.demo.log('new() · constructed');
this.get(() => {
this.demo.log('effect · committed');
const measure = () => (this.width = window.innerWidth);
const timer = setInterval(() => this.ticks++, 1000);
measure();
window.addEventListener('resize', measure);
return () => {
this.demo.log('effect cleanup · unmounted');
clearInterval(timer);
window.removeEventListener('resize', measure);
};
});
return () => this.demo.log('new() cleanup · destroyed');
}
box = ref<HTMLDivElement>(() => {
this.demo.log('ref() · element attached');
return () => this.demo.log('ref() · element detached');
});
render() {
const { width, ticks } = this;
return (
<div className="probe" ref={this.box}>
<strong>{width}px</strong>
<small>alive {ticks}s</small>
</div>
);
}
}
App.css
.row {
display: flex;
gap: var(--s2);
}
.probe {
display: flex;
flex-direction: column;
gap: var(--s1);
width: 100%;
padding: var(--s4);
background: var(--surface);
border: 1px solid var(--accent);
border-radius: var(--r);
}
.probe strong {
font-family: var(--font-mono);
font-size: var(--t-xl);
color: var(--accent);
}
.trace {
width: 100%;
margin: 0;
padding: 0;
list-style: none;
text-align: left;
font-family: var(--font-mono);
font-size: var(--t-xs);
border: 1px solid var(--border);
border-radius: var(--r);
overflow: hidden;
}
.trace:empty::after {
content: 'nothing logged yet';
display: block;
padding: var(--s3);
color: var(--muted);
}
.trace li {
padding: var(--s2) var(--s3);
border-radius: 0;
cursor: default;
color: var(--fg-soft);
border-top: 1px solid var(--border);
}
.trace li:first-child {
border-top: none;
}
.trace li:hover {
background: none;
}