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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ichi-h/elmish

v2.0.0

Published

A state management library that is independent of UI frameworks and UI libraries, with reference to Elm Architecture.

Downloads

22

Readme

@ichi-h/elmish

LICENSE npm version

日本語版はこちら

@ichi-h/elmish is a state management library that is independent of UI frameworks and UI libraries, with reference to Elm Architecture. This library aims to keep business logic separate from other complex factors and keep it readable.

Usage

Install

npm install @ichi-h/elmish

Write logic

// data.ts
import { elmish } from "@ichi-h/elmish";

export type Model = {
  count: number;
  loader: "idle" | "loading";
};

export type Message =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "startReset" }
  | { type: "endReset" };

export const init: Model = {
  count: 0,
  loader: "idle",
} as const;

export const useElement = elmish<Model, Message>();
// update.ts
import { Update } from "@ichi-h/elmish";

import { Model, Message } from "./data";

export const update: Update<Model, Message> = (model, message) => {
  switch (message.type) {
    case "increment": {
      return { ...model, count: model.count + 1 };
    }

    case "decrement": {
      return { ...model, count: model.count - 1 };
    }

    case "startReset": {
      return [
        { ...model, loader: "loading" },
        async () => {
          return new Promise((resolve) => {
            setTimeout(() => {
              resolve({ type: "endReset" });
            }, 1000);
          });
        },
      ];
    }

    case "endReset": {
      return { ...model, count: 0, loader: "idle" };
    }
  }
};

Use in Vanilla TypeScript

// counter.ts
import { Model, init, useElement } from "./data";
import { update } from "./update";

export function setupCounter(
  counter: HTMLParagraphElement,
  incrementBtn: HTMLButtonElement,
  decrementBtn: HTMLButtonElement,
  resetBtn: HTMLButtonElement,
) {
  const updateView = (newModel: Model) => {
    if (newModel.loader === "loading") {
      counter.innerHTML = "loading...";
    } else {
      counter.innerHTML = `count is ${newModel.count}`;
    }
  };

  const send = useElement(init, update, updateView);

  incrementBtn.addEventListener("click", () => send({ type: "increment" }));
  decrementBtn.addEventListener("click", () => send({ type: "decrement" }));
  resetBtn.addEventListener("click", () => send({ type: "startReset" }));

  send({ type: "startReset" });
}
// main.ts
import { setupCounter } from "./counter";

document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
  <div>
    <button id="decrement" type="button">-</button>
    <button id="reset" type="button">reset</button>
    <button id="increment" type="button">+</button>
    <p id="counter"></p>
  </div>
`;

setupCounter(
  document.querySelector<HTMLParagraphElement>("#counter")!,
  document.querySelector<HTMLButtonElement>("#increment")!,
  document.querySelector<HTMLButtonElement>("#decrement")!,
  document.querySelector<HTMLButtonElement>("#reset")!,
);

Use in React

import { init, useElement } from "./data";
import { update } from "./update";

export const App = () => {
  const [model, setModel] = useState(init);

  const send = useElement(model, update, setModel);

  const increment = () => send({ type: "increment" });
  const decrement = () => send({ type: "decrement" });
  const reset = () => send({ type: "startReset" });

  return (
    <div>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>reset</button>
      <button onClick={increment}>+</button>
      {model.loader === "loading" && <p>loading...</p>}
      {model.loader === "idle" && <p>count is {model.count}</p>}
    </div>
  );
};

Use in Vue

<script setup lang="ts">
import { ref } from "vue";

import { Model, init, useElement } from "./data";
import { update } from "./update";

const model = ref(init);
const updateView = (newModel: Model) => (model.value = newModel);

const send = useElement(model.value, update, updateView);

const increment = () => send({ type: "increment" });
const decrement = () => send({ type: "decrement" });
const reset = () => send({ type: "startReset" });
</script>

<template>
  <div>
    <button type="button" @click="decrement">-</button>
    <button type="button" @click="reset">reset</button>
    <button type="button" @click="increment">+</button>
    <p v-if="model.loader === 'loading'">loading...</p>
    <p v-else-if="model.loader === 'idle'">count is {{ model.count }}</p>
  </div>
</template>

What is Elm Architecture?

The Elm Architecture is an architecture for building web applications that uses the Elm language. Elm Architecture is no longer limited to web front-ends, but has been adopted as the architecture for a variety of applications that require interaction, such as desktop apps, mobile apps, and games.

I'm not good enough to explain this, so I quote the documentation for an overview of Elm Architecture.

Diagram of The Elm Architecture

The Elm program produces HTML to show on screen, and then the computer sends back messages of what is going on. "They clicked a button!"

What happens within the Elm program though? It always breaks into three parts:

  • Model — the state of your application
  • View — a way to turn your state into HTML
  • Update — a way to update your state based on messages

These three concepts are the core of The Elm Architecture.

- The Elm Architecture · An Introduction to Elm

See Guide to the Elm Language for more information. It is very informative!

@ichi-h/elmish's philosophy.

Business logic married with UI library

Many state management systems in this world are designed to rely on UI libraries. This is necessary to achieve a declarative UI. This is necessary for a declarative UI, because when the state is updated, the UI can automatically change without manipulating the DOM. This is very convenient!

However, this often leads to tightly coupling business logic with UI libraries and state management systems. Let's write a simple counter application that satisfies the following requirements in the common React writing style.

  • Clicking the + button increases the count by 1.
  • Clicking the - button decreases the count by 1.
  • When the reset button is clicked, the count on the screen changes to "loading..." and after 1 second, the count becomes 0 and the count is displayed again.
export const App = () => {
  const [count, setCount] = useState(0);
  const [loader, setLoader] = useState<"idle" | "loading">("idle");

  const increment = () => setCount((prev) => prev + 1);
  const decrement = () => setCount((prev) => prev - 1);
  const reset = () => {
    setLoader("loading");
    setTimeout(() => {
      setCount(0);
      setLoader("idle");
    }, 1000);
  };

  return (
    <div>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>reset</button>
      <button onClick={increment}>+</button>
      {loader === "loading" && <p>loading...</p>}
      {loader === "idle" && <p>count is {count}</p>}
    </div>
  );
};

It is important to note that React does not appear in any of the above use cases, which is natural since React is only a means to achieve the use cases.

So what does the actual code look like? I used useState to manage the value of count. This means that the business logic of the use case that the counter should implement is embedded in the React ecosystem.

What is the problem with this?

For example, if there are some breaking changes in React, you need to fix that, but if your business logic is tightly coupled with React, you are very likely to rewrite the business logic together, even though the business logic has not changed.

And what if a more attractive UI library than React appears and you have to replace it? This is very hard. Because you would literally have to rewrite almost all of the code.

Also, if the React-specific writing style is included in the business logic, it may become noise and make it difficult to see the essential aspects of the logic.

Of course, if the example were this simple, it would not be a problem. However, production business logic is much more complex, and state management can be cumbersome. In such an environment, the separation of UI library and business logic becomes more important.

Keep your business logic readable!

In @ichi-h/elmish, the UI update logic is injected from the outside so that the business logic can be decoupled from the UI library.

As shown in Usage example, React does not appear in data.ts and update.ts. Let's concentrate on writing business logic here.

Then choose what method to use to display the UI, depending on the situation.

Why use @ichi-h/elmish?

The benefit of using @ichi-h/elmish is almost the same as separating the business logic from the UI library. In other words:

  • Concentrate on writing business logic based on the Elm Architecture without worrying about how to write UI libraries.
  • Even if there are disruptive changes to the UI library, your business logic can be protected from those changes.
  • Eliminating the UI library-specific writing style keeps noise out of your business logic and keeps the code easy to understand.
  • Your business logic can be reused when replacing UI libraries.

Why NOT use @ichi-h/elmish?

After hearing all this, you may be thinking: "Having business logic dependent on @ichi-h/elmish is not clean!" This is definitely correct. If there are destructive changes to this library, you may need to rewrite the business logic. Therefore, if clean code is important to you, you should not use this library.

If you insist on cleanliness, you can refer to designs such as DDD and Clean Architecture, and if you are more concerned about Elm Architecture, you can also implement a mechanism like this library on your own.

License

@ichi-h/elmish is licensed under the MIT License. See LICENSE for the full license text.