set() Factory

set() Factory demo built with Expressive MVC - the complete source below runs as an editable sandbox when JavaScript is enabled.

App.tsx

import './App.css';

import { Component, set } from '@expressive/react';
import { Suspense } from 'react';

export default () => (
  <div className="container">
    <h1>Factories</h1>
    <p>
      A zero-argument function makes the slot a factory: it runs on first access,
      caches its result, and the field is read-only. An <code>async</code> factory
      throws suspense while it pends, so the boundary above decides what waiting
      looks like - no loading flag, no effect, no deps.
    </p>
    <p>
      Factories are bound to the instance, so they read sibling fields freely -{' '}
      <code>greeting</code> is synchronous yet waits anyway, because reading a
      pending field suspends it too. Resolution cascades.
    </p>
    <Suspense fallback={<p className="pending">loading profile…</p>}>
      <Profile />
    </Suspense>
    <p>
      Pass <code>false</code> and a factory stops suspending: the field reads{' '}
      <code>undefined</code> until it resolves, then updates like any other.
    </p>
    <Sidebar />
    <small>
      Nothing here is fetched twice - each factory runs once per instance, on the
      first read that needs it.
    </small>
  </div>
);

class Profile extends Component {
  user = set(async () => {
    return await after(700, { name: 'Ada Lovelace', role: 'Engineer' });
  });

  greeting = set(() => {
    const [first] = this.user.name.split(' ');

    return `Welcome back, ${first}`;
  });

  render() {
    const { user, greeting } = this;

    return (
      <section className="card">
        <h2>{greeting}</h2>
        <small>{user.role}</small>
      </section>
    );
  }
}

class Sidebar extends Component {
  followers = set(async () => {
    return await after(1500, 1204);
  }, false);

  render() {
    const { followers } = this;

    return (
      <p className="pending">
        {followers === undefined
          ? 'counting followers…'
          : `${followers.toLocaleString()} followers`}
      </p>
    );
  }
}

function after<T>(ms: number, value: T) {
  return new Promise<T>((resolve) => setTimeout(resolve, ms, value));
}

App.css

.card {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  width: 100%;
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
}

.card h2 {
  margin: 0;
  font-size: var(--t-xl);
}

.pending {
  margin: 0;
  padding: var(--s2) var(--s4);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  color: var(--fg-soft);
  background: var(--code-bg);
  border: 1px solid var(--border);
  border-radius: 999px;
}