npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hypur/grain

v0.0.24

Published

An extremely lightweight (1.26 KB gziped) package to easily introduce reactivity to to server-side applications.

Readme

@hypur/grain

An extremely lightweight (1.26 KB gziped) package to easily introduce reactivity to to server-side applications.

Installation

Use your favorite package manager to install @hypur/grain

Via NPM

npm install @hypur/grain

Via yarn

yarn add @hypur/grain

Via Bun

bun add @hypur/gain

Creating a Grain

Creating your first grain is easy. Start in your html file. Let's have a button be a grain:

<div>
  <span>I have not been clicked</span>
  <button is="clicker-button">Click me</button>
</div>

Then, create a class to represents this grain:

class ButtonGrain extends Grain {
  constructor() {
    super();
  }
}

Let's say we want to react to clicker-button being clicked. To do this, we can override the onClick function:

class ButtonGrain extends Grain {
  constructor() {
    super();
  }

  override onClick() {
    console.log("I was clicked!");
  }
}

Now let's mount this grain using Grain.mount to see the magic happen:

class ButtonGrain extends Grain {
  constructor() {
    super();
  }

  override onClick() {
    console.log("I was clicked!");
  }
}

Grain.mount("clicker-button", ButtonGrain);

In the console you should now see "I was clicked!" when you click clicker-button!

Creating a Reactive Grain

This builds off of the Creating a Grain tutorial. Recall the ButtonGrain we created:

<div>
  <span>I have not been clicked</span>
  <button is="clicker-button">Click me</button>
</div>
class ButtonGrain extends Grain {
  constructor() {
    super();
  }

  override onClick() {
    console.log("I was clicked!");
  }
}

Grain.mount("clicker-button", ButtonGrain);

Let's say we want to count the number of times a grain is clicked. To do this, we must extends ReactiveGrain instead of Grain to introduce state for ButtonGrain:

class ButtonGrain extends ReactiveGrain {
  constructor() {
    super();
  }

  override onClick() {
    console.log("I was clicked!");
  }
}

I'm using typescript here, so let's create an interface so our state is typed. This can be passed as a generic into ReactiveGrain. Let's also initialize state in our constructor super() call:

interface IButtonGrainState {
  clicks: number;
}

class ButtonGrain extends ReactiveGrain<IButtonGrainState> {
  constructor() {
    super({
      clicks: 0,
    });
  }

  override onClick() {
    console.log("I was clicked!");
  }
}

We'll want to increment counts when the button is clicked, so let's update ButtonGrain's state in the onClick function:

interface IButtonGrainState {
  clicks: number;
}

class ButtonGrain extends ReactiveGrain<IButtonGrainState> {
  constructor() {
    super({
      clicks: 0,
    });
  }

  override onClick() {
    this.state.clicks++;
  }
}

That's nice, but we probably want to reflect state updates on our UI. Let's update our HTML to add an is attribute to our span element:

<div>
  <span is="clicker-count">I have not been clicked</span>
  <button is="clicker-button">Click me</button>
</div>

Now, we can use Sow to locate click-count and update its contents. Read Locating Elements to learn more about Sow:

interface IButtonGrainState {
  clicks: number;
}

class ButtonGrain extends ReactiveGrain<IButtonGrainState> {
  constructor() {
    super({
      clicks: 0,
    });
  }

  override onClick() {
    this.state.clicks++;
    Sow.first(
      "clicker-count"
    ).innerText = `I have been clicked ${this.state.clicks} times`;
  }
}

Now we have a simple counter clicker app in roughly 12 lines of client-side code!

Locating Elements

Use helper methods available via Sow to locate and work with other grains. Suppose you wanted to create a show/hide interface:

<div>
  <p is="show-hide-message">I am visible!</p>
  <button>Hide message above</button>
</div>
class ShowHideButton extends Grain {
  constructor() {
    super();
  }

  override onClick() {
    const showHideMessage = Sow.first("show-hide-message");
    showHideMessage.hidden = !showHideMessage.hidden;
  }
}

Grain.mount("show-hidden-button", ShowHideButton);

Sow works by using the name you pass and compares it to elements with an is attribute. It returns an HTMLElement, so all your regular DOM methods are available.

Creating a Form Grain

Forms are essential to the web. Using a GrainForm you can add client side reactativity to a form element.

Consider the following signup form and related grain:

<form method="POST" action="/submit" is="signup-form">
  <input name="username" type="text" value="" required />
  <input name="password" type="password" value="" required />
</form>
interface ISignupFormData {
  username: string;
  password: string;
}

class SignupForm extends GrainForm<ISignupFormData> {
  constructor() {
    super();
  }
}

Grain.mount("signup-form", SignupForm);

We already have an interface for our data we expect the form to record. GrainForm accepts this as a generic, similar to how this is done in a ReactiveGrain for state.

Let's console.log the data recieved from the form. We can do this by creating an override for handleData:

interface ISignupFormData {
  username: string;
  password: string;
}

class SignupForm extends GrainForm<ISignupFormData> {
  constructor() {
    super();
  }

  override handleData(data: ISignupFormData) {
    console.log(data);
  }
}

Grain.mount("signup-form", SignupForm);

Nice! If you also check the network tab, the a POST request to /submit will be made since action="/submit and method="POST" on our form elements. If we didn't want a network request to happen, these attributes can be removed.

Suppose we wanted to handle the response recieved from server after making the POST to /submit. This also isn't hard. Simply make an override for handleResponse, which accepts a Response object. We want to get the json of the Response, so we'll make handleResponse an async function:

interface ISignupFormData {
  username: string;
  password: string;
}

class SignupForm extends GrainForm<ISignupFormData> {
  constructor() {
    super();
  }

  override async handleResponse(response: Response) {
    const json = await response.json();
    console.log(json);
  }
}

Grain.mount("signup-form", SignupForm);

Neat! What about hypermedia, though? We can use Sow to replace our form with the hypermedia sent from the server:

interface ISignupFormData {
  username: string;
  password: string;
}

class SignupForm extends GrainForm<ISignupFormData> {
  constructor() {
    super();
  }

  override async handleResponse(response: Response) {
    const hypermedia = await response.text();
    Sow.first("signup-form").outerHTML = hypermedia;
  }
}

Grain.mount("signup-form", SignupForm);

Sharing State Between a Grain and the Server

We don't want your state to feel trapped. If there is a state that lives on the server that needs to the piped down to a grain, take advantage of HTML attributes.

Take our button clicker example from (Creating a Reactive Grain)["/creating-a-reactive-grain.md"]:

<div>
  <span is="clicker-count">I have not been clicked</span>
  <button is="clicker-button">Click me</button>
</div>
interface IButtonGrainState {
  clicks: number;
}

class ButtonGrain extends ReactiveGrain<IButtonGrainState> {
  constructor() {
    super({
      clicks: 0,
    });
  }

  override onClick() {
    this.state.clicks++;
    Sow.first(
      "clicker-count"
    ).innerText = `I have been clicked ${this.state.clicks} times`;
  }
}

Grain.mount("clicker-button", ButtonGrain);

Let's assume our backend can interpolate the current button count in the p tag' such that if the count is 5, it would return:

<div>
  <span is="clicker-count">I have been clicked 5 times!</span>
  <button is="clicker-button">Click me</button>
</div>

To have 5 be used by our grain as the starting count, let's create an attribute current-count="5":

<div>
  <span is="clicker-count" current-count="5">I have been clicked 5 times!</span>
  <button is="clicker-button">Click me</button>
</div>

We can then use this in the constructor of our grain:

interface IButtonGrainState {
  clicks: number;
}

class ButtonGrain extends ReactiveGrain<IButtonGrainState> {
  constructor() {
    const currentCount = this.getAttribute("current-count");
    super({
      clicks: Number(currentCount),
    });
  }

  override onClick() {
    this.state.clicks++;
    Sow.first(
      "clicker-count"
    ).innerText = `I have been clicked ${this.state.clicks} times`;
  }
}

Grain.mount("click-button", ButtonGrain);

this.getAttribute("current-count") is all we need to get that data from the html and save it into ButtonGrain's state. Note that any value mapped to an attribute is a string, requiring casting as appropriate.

Say we now want to save the current count when the user stops clicking. We can use this.put, which sends a PUT request to the server with ButtonGrain's state, inside an override for a onMouseLeave:

interface IButtonGrainState {
  clicks: number;
}

class ButtonGrain extends ReactiveGrain<IButtonGrainState> {
  constructor() {
    const currentCount = this.getAttribute("current-count");
    super({
      clicks: Number(currentCount),
    });
  }

  override onClick() {
    this.state.clicks++;
    Sow.first(
      "clicker-count"
    ).innerText = `I have been clicked ${this.state.clicks} times`;
  }

  override onMouseLeave() {
    this.put("/save");
  }
}

Grain.mount("clicker-button", ButtonGrain);

That's it! In roughly 20 lines of code, we have an interface that shares state with a server!