Context

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

App.tsx

import './App.css';

import State, { Consumer, Provider } from '@expressive/react';

const SHELF = [
  { name: 'Espresso', price: 3 },
  { name: 'Cortado', price: 4 },
  { name: 'Pour-over', price: 5 }
];

export default () => (
  <div className="container">
    <h1>Context</h1>
    <p>
      <code>Provider</code> puts state in context - a class it constructs and will
      destroy, an instance it leaves alone, or a map of several at once. Anything
      below finds it by class with <code>.get()</code>, which is the difference
      between joining state and creating it.
    </p>
    <Provider for={{ shop: Shop, cart: Cart }}>
      <div className="counter">
        <Greeting />
        <Shelf />
        <Badge />
        <Consumer for={Cart}>
          {(cart) => <p className="total">Total ${cart.total}</p>}
        </Consumer>
      </div>
    </Provider>
    <small>
      Subscriptions are per component and per field: the badge tracks{' '}
      <code>count</code>, the total tracks <code>total</code>, and the greeting
      re-renders for neither. <code>is</code> is the instance itself - reads
      through it don't subscribe, which is how the shelf writes to a cart it never
      displays.
    </small>
  </div>
);

class Shop extends State {
  barista = 'Ada';
}

class Cart extends State {
  count = 0;
  total = 0;

  add(price: number) {
    this.count++;
    this.total += price;
  }
}

const Greeting = () => {
  const { barista } = Shop.get();

  return <p className="greeting">{barista} is on bar</p>;
};

const Shelf = () => {
  const { is: cart } = Cart.get();

  return (
    <div className="shelf">
      {SHELF.map(({ name, price }) => (
        <button key={name} onClick={() => cart.add(price)}>
          {name} <small>${price}</small>
        </button>
      ))}
    </div>
  );
};

const Badge = () => {
  const { count } = Cart.get();

  return <p className="badge">{count} in cart</p>;
};

App.css

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

.greeting {
  margin: 0;
  font-size: var(--t-sm);
  color: var(--muted);
}

.shelf {
  display: flex;
  gap: var(--s2);
}

.shelf button {
  flex: 1;
}

.badge,
.total {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--t-sm);
}

.badge {
  color: var(--muted);
}

.total {
  color: var(--accent);
}