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 { Global, Session, Theme, Viewport } from './Global';
export default () => (
<Global>
<div className="container">
<h1>App-wide state</h1>
<p>
The app's top component owns three states and provides them with itself,
so anything below reaches each by type.
</p>
<div className="cards">
<Size />
<Account />
<Appearance />
</div>
<small>
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>
</Global>
);
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);
}
Global.ts
import { Component, Model } from '@expressive/mvc';
export class Global extends Component {
viewport = new Viewport();
session = new Session();
theme = new Theme();
}
export class Viewport extends Model {
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);
}
}
export class Session extends Model {
user: string | null = null;
login() {
this.user = 'Ada';
}
logout() {
this.user = null;
}
}
export class Theme extends Model {
dark = prefersDark();
toggle() {
this.dark = !this.dark;
}
protected new() {
return this.get(({ dark }) => {
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
});
}
}
// Read the theme the document already has, or the effect above paints
// light over a dark page on activation.
function prefersDark() {
const { theme } = document.documentElement.dataset;
if (theme) return theme === 'dark';
return matchMedia('(prefers-color-scheme: dark)').matches;
}