Globals

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

App.tsx

import './App.css';

import { Session } from './Session';
import { Theme } from './Theme';
import { Viewport } from './Viewport';

Viewport.new();
Session.new();
Theme.new();

export default () => (
  <div className="container">
    <h1>Globals</h1>
    <p>
      Three states created once up here, the way you would beside the root render.
      Reaching one anywhere takes two halves: the class declares{' '}
      <code>static global</code>, and <code>.new()</code> activates it. Activation
      alone leaves an instance private to whoever made it - which is what keeps a
      forgotten Provider from installing per-request state process-wide.
    </p>
    <div className="cards">
      <Size />
      <Account />
      <Appearance />
    </div>
    <small>
      No Provider anywhere. Each card calls <code>.get()</code> and subscribes to
      only the fields it reads - resize the window and just the first one moves.
    </small>
  </div>
);

const Size = () => {
  const { width, compact } = Viewport.get();

  return (
    <article className="card">
      <h2>Viewport</h2>
      <b>{width}px</b>
      <small>{compact ? 'compact layout' : 'wide layout'}</small>
    </article>
  );
};

const Account = () => {
  const { user, login, logout } = Session.get();

  return (
    <article className="card">
      <h2>Session</h2>
      <b>{user ?? 'signed out'}</b>
      {user ? (
        <button onClick={logout}>Log out</button>
      ) : (
        <button onClick={login}>Log in</button>
      )}
    </article>
  );
};

const Appearance = () => {
  const { dark, toggle } = Theme.get();

  return (
    <article className="card">
      <h2>Theme</h2>
      <b>{dark ? 'dark' : 'light'}</b>
      <button onClick={toggle}>Switch</button>
    </article>
  );
};

App.css

.cards {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  width: 100%;
}

.card {
  display: flex;
  flex: 1 1 7rem;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
  padding: var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  text-align: left;
}

.card h2 {
  margin: 0;
  font-size: var(--t-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.card b {
  font-family: var(--font-mono);
  font-size: var(--t-lg);
  color: var(--accent);
}

.card button {
  align-self: stretch;
  margin-top: auto;
  font-size: var(--t-sm);
}

Session.ts

import State from '@expressive/react';

export class Session extends State {
  // One session for the whole app - the declaration is what makes "the whole
  // app" true of it, rather than a convention the imports happen to follow.
  static global = true;

  user: string | null = null;

  login() {
    this.user = 'Ada';
  }

  logout() {
    this.user = null;
  }
}

Theme.ts

import State from '@expressive/react';

export class Theme extends State {
  // Global by nature: there is only one document to paint.
  static global = true;

  dark = prefersDark();

  toggle() {
    this.dark = !this.dark;
  }

  protected new() {
    return this.get(({ dark }) => {
      document.documentElement.dataset.theme = dark ? 'dark' : 'light';
    });
  }
}

// The document arrives already themed - by the page hosting this example, or by
// the OS. Read that first, because the effect above paints on activation and
// would otherwise light up a dark page until the first toggle.
function prefersDark() {
  const { theme } = document.documentElement.dataset;

  if (theme) return theme === 'dark';

  return matchMedia('(prefers-color-scheme: dark)').matches;
}

Viewport.ts

import State from '@expressive/react';

// Display-agnostic logic with no render() of its own: mutable inputs are
// fields, derived values are getters. A global belongs to the app, not a
// component, so setup and teardown live in new() - browser-only code here;
// an app that renders on the server would guard its window access.
export class Viewport extends State {
  // Reachable app-wide. Without this line the instance is private to whoever
  // created it - everything here still works, but Viewport.get() finds nothing.
  static global = true;

  width = window.innerWidth;

  get compact() {
    return this.width < 600;
  }

  protected new() {
    const update = () => (this.width = window.innerWidth);

    window.addEventListener('resize', update);

    return () => window.removeEventListener('resize', update);
  }
}