Fetch

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

App.tsx

import './App.css';

import { Query } from './Query';

// Extending Query gives us all the request bookkeeping for free.
// We just supply `request()` - the actual endpoint-specific work -
// and inherit waiting/error tracking, run, and reset.

class HelloQuery extends Query {
  async request() {
    const res = await fetch('https://randomuser.me/api?nat=us&results=1');
    const { first, last } = (await res.json()).results[0].name;
    return `Hello ${first} ${last}`;
  }
}

const Example = () => {
  // `.use()` creates an instance scoped to App.
  // Destructuring subscribes us to those fields.
  const { response, error, waiting, reset, run } = HelloQuery.use();

  if (response) 
    return (
      <p className="fetch-result">
        <span>Server said: {response}</span>
        <button onClick={reset}>Reset</button>
      </p>
    )

  if (error) 
    return (
      <p className="fetch-result">
        <span>Error: {error.message}</span>
        <button onClick={reset}>Reset</button>
      </p>
    )

  if (waiting) 
    return <p className="fetch-status">Sent. Waiting on response...</p>

  return (
    <button className="fetch-action" onClick={run}>Say hello to server</button>
  )
};

export default () => (
  <div className="container">
    <h1>Fetch Example</h1>
    <Example />
  </div>
);

App.css

.fetch-action {
  align-self: center;
}

.fetch-result {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  margin: 0;
  color: var(--fg-soft);
}

.fetch-status {
  margin: 0;
  color: var(--muted);
}

Query.ts

import State from '@expressive/react';

// Most async work has the same shape - we wait for a response, get
// something back, sometimes get an error. Query handles that scaffolding
// so any specific endpoint only plugs in the part that's actually different.

export abstract class Query extends State {
  // Three flags covering the lifecycle of a request.
  // A component reading any of these refreshes when it changes.
  response?: any = undefined;
  error?: Error = undefined;
  waiting = false;

  // Subclasses supply the actual fetch. Whatever you return
  // becomes `response`; throw to populate `error`.
  abstract request(): Promise<any>;

  // Call `run()` to kick things off. Assignments to `this` along the
  // way refresh anyone subscribed - no manual notify, no setState.
  async run() {
    this.waiting = true;

    try {
      this.response = await this.request();
    } catch (error) {
      if (error instanceof Error) this.error = error;
    } finally {
      this.waiting = false;
    }
  }

  reset() {
    this.response = undefined;
    this.error = undefined;
  }
}