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;
}