DEV Community

Cover image for Build a live-updating stock price badge for your web
katka
katka

Posted on

Build a live-updating stock price badge for your web

Yuzu API is a free market data API that can power UIs of any size: from huge sprawling webapps to drop-in UI components.

In this example we'll focus on powering something on the small scale: a portable web component that renders a badge with live-updating stock prices, like this:

Live badge example

If you'd rather skip to the good part, my friend Steve created a GitHub Repo with our demo code. You can drop it right into your existing page!


Since we want this component to be fully portable and framework-agnostic, we'll be using Lit + Typescript to create a native Web Component. This guide assumes you have the following tools installed:

  • Node.js
  • Yarn

To get started, we'll use yarn to create a project for us.

$ yarn create vite yuzu-stock-price-badge --template lit-ts
$ cd yuzu-stock-price-badge
$ yarn install`
Enter fullscreen mode Exit fullscreen mode

The code above creates a new project folder using Vite as the build tool, then moves into the directory and installs dependencies.

Once we're in our new project and have run a fresh yarn install, let's do a few things.

  1. Vite has created one component for us at src/my-element.ts. Let's rename it to yuzu-price-badge.ts.

  2. In src/yuzu-price-badge.ts, let's replace all occurences of my-element with yuzu-price-badge.

  3. In package.json, index.html, and vite.config.ts, go ahead and do the same thing, replacing all usages of my-element with yuzu-price-badge.

  4. Run yarn build and make sure everything looks ok!

If that sounds like a lot of work, you can just run these commands in your terminal instead:

mv src/my-element.ts src/yuzu-price-badge.ts
sed -i '' 's/my-element/yuzu-price-badge/g' vite.config.ts \
  src/yuzu-price-badge.ts \
  package.json \
  index.html
sed -i '' 's/MyElement/YuzuPriceBadge/g' src/yuzu-price-badge.ts
Enter fullscreen mode Exit fullscreen mode

Next, let's clean out all the boilerplate from src/yuzu-stock-price-badge.ts

The file should look about this this when you're done:

src/yuzu-price-badge.ts

import { html, css, LitElement } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'

/**
 * Yuzu stock price badge
 */
@customElement('yuzu-price-badge')
export class YuzuPriceBadge extends LitElement {
  static styles = css`
    :host {
    }
  `

  render() {
    return html``
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'yuzu-price-badge': YuzuPriceBadge
  }
}
Enter fullscreen mode Exit fullscreen mode

Ok, now that we're at a good baseline, let's add some properties to our new component, just above our render method:

src/yuzu-price-badge.ts

  /**
   * Your Yuzu API key, exposed in HTML as "api-key"
   */
  @property({ type: String, attribute: "api-key" })
  apiKey = "";

  /**
   * The symbol to render
   * E.g. AAPL, MSFT, BRK.A
   */
  @property({ type: String })
  symbol = "";

  /**
   * Hold the last price of our stock
   */
  @state()
  private lastPrice: number = 0;
Enter fullscreen mode Exit fullscreen mode

Our two new properties, apiKey and symbol will be exposed in HTML to change what's rendered in this badge. The third property, lastPrice is a bit of internal state that will hold the last known price of our stock. Let's go to index.html and update our component.

index.html

 <body>
    <!-- Replace "demo" with your own API key -->
    <yuzu-price-badge api-key="demo" symbol="BYND">
    </yuzu-price-badge>
  </body>
Enter fullscreen mode Exit fullscreen mode

Great! Now, we need to fetch the last price of our stock when the component loads. We can override the connectedCallbackfunction from LitElement to do that. Let's put our connectedCallback between our new properties and the rendermethod.

src/yuzu-price-badge.ts

  async connectedCallback() {
    super.connectedCallback();
  }
Enter fullscreen mode Exit fullscreen mode

Next, we're going to do two things:

  • Make a GraphQL request to grab the last price of the stock.
  • Subscribe to the live price stream for our stock, and update our internal state when the price changes.

Because Yuzu's streaming APIs only returns data on stocks during market hours, it's good practice to use the GraphQL API to fetch any data you need to render your UI, and then subscribe to subsequent updates.

Let's add the following code to our connectedCallback to make the GraphQL request:

async connectedCallback() {
    super.connectedCallback();

    // Visit https://graph.yuzu.dev/graphql to see docs on the whole graph
    const query = `query {
      securities(input: { symbols: ["${this.symbol}"] }) {
        lastTrade {
          price
        }
      }
    }`

    const graphQlResult = await fetch("https://graph.yuzu.dev/graphql", {
      method: "POST",
      body: JSON.stringify({
        query
      }),
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': "application/json",
      }
    });
    const { data } = await graphQlResult.json();

    this.lastPrice = parseFloat(data.securities[0].lastTrade.price);
  }
Enter fullscreen mode Exit fullscreen mode

Great! Here's a quick breakdown of what's happening above:

  1. Construct the body of our GraphQL query, requesting securities matching our symbol, and on that object, the price field of that security's last trade.

  2. Using the Fetch API to make the request, parsing the result, and assigning the value to this.lastPrice.

One last step before we can see the fruits of our work: rendering lastPrice. Let's modify render() to show our symbol and the saved price.

  render() {
    return html`
    <p>${this.symbol}</p>
    <p>$${this.lastPrice.toFixed(2)}</p>
    `
  }
Enter fullscreen mode Exit fullscreen mode

Now, run yarn dev in your terminal and open up localhost:3000 in your browser. You should see the last price of BYND.

All we need to do now is add some liveness! Thankfully this is trivial using Yuzu streams and the EventSource API. Let's add the following code to the bottom of connectedCallback():

async connectedCallback() {
    /*
    ... Our GraphQL querying code
    */

    const stream = `S:T:${this.symbol}`;
    const url = `https://sse.yuzu.dev/sse?streams=${stream}&token=${this.apiKey}`;
    const tradeStream = new EventSource(url);
    tradeStream.addEventListener("message", (message) => {
      this.lastPrice = parseFloat(JSON.parse(message.data).price);
    });
  } 
Enter fullscreen mode Exit fullscreen mode

This is just a few lines of code, but there's a lot of power behind them. Here's what's happening:

  1. We construct a URL pointing at Yuzu's server-sent events API, including our API token and the stream name S:T:BYND, which asks for every trade of the Security BYND

  2. Add an event listener to the EventSource which parses each message, and assigns the value of price to our internal state, causing the component to re-render.

Now, if you go back to your browser, you'll see the price updating in real-time as long as the markets are open.

Now, all that's left to do is add some styling!

static styles = css`
    :host {
      padding: 0.3rem 0.5rem;
      display: inline-flex;
      flex-direction: row;
      gap: 0.5rem;
      border: 2px solid black;
      border-radius: 99rem;
      background-color: gainsboro;

      font-family: sans-serif;
      font-weight: bold;
      font-size: 1rem;
    }

    p {
      margin: 0;
    }
  `
Enter fullscreen mode Exit fullscreen mode

And there have it! Check out our example repo for a ready-made version you can drop right into your existing page.

Top comments (0)