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