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

inertjs

v0.1.25

Published

An atomic state management library for React

Downloads

10

Readme

What is Charm

Charm is an atomic state management library for React.

Latest documentation is available at https://jsr.io/@kaigorod/charm/doc


// counterCharm.ts 

const counterCharm = charm(0);

export const useCounter = () => useCharm(counterCharm);
export const inc = () => counterCharm.update(prev => prev + 1);

// Counter.tsx

const Counter = () => {
  const counter = useCounter();

  return <button onclick={inc}>
    clicked {counter} times
  </button>
}

State management best practices

Atoms stay protected

Notice that counterCharm is not exported directly. This way we avoid un-expected direct modifications of the atom we created.

Instead, we create and expose micro-API to interact with the particle.

useCounter

useCounter() is extremly simple to use, it does two things.

First, it returns the value of the current value of the counter.

Second, as any React hook, it subscribes the component to changes of the counter. So, when a counter changes then the components re-renders.

Avoid setter callback hooks

Notice, where is no setCounter function returned by the useCharm hook. One of the distiguishing feature of Charm is that your Component doesn't have to subscribe to the changes of the hooks.

You can update Charm value from simple functions. You don't need to call hook to create function on every render.

There several benefit for this:

  • component doesn't have to re-render if they use just the update callbacks
  • with less re-renders the app is getting better performance
  • you don't have to write useCallback, useMemo and other cache code for you update callbacks.
  • you avoid having complicate cached callback bugs with useEffect and event listeners.
  • you write less code, and your code is more readable

So, this way the only case your components got re-render is the actual change of the atom they implicitly subscribed to using the use-Charm hook.

How update callbacks work internally

Modern state management libaries, in addition to managing front-end state have to provide compatibility with Server-Side Rendering (SSR) and Server-Side Generation (SSG) rendering. In this mode, the page of the application are rendered in-parallel. This way every atom sits in the memory of the node application in multiple instances, one per every page being rendered. A specific state of all the atom and variables which is required to render a single page call Store.

Let's look at the example of a todo app which uses server-side rendering. When users request of the app simultateously request their todo-list page at the same time then their todo-list pages render separately and in parallel. One page and one store for every single user request. For different user the page has different data to show. But, the nodejs application has single memory space where it creates atoms, functions and other variables. Store

To distinguish page renders tools like redux, recoil, jotai use ReactContext-based approach. They use ReactContext providers at the root of the render tree and then use ReactContext on the leaves of the render tree. This why are force to create callback using hooks in order to pass the rendering context to the hooks.

Unlike other libs, Charm is using AsyncLocalStorage to pass rendering context to the callbacks and other functions. This way Charm stay independant from React context and does not require developers to write hooks for callbacks.

Best practices, enabled

Don't Repeat Yourself

Data consistancy

When you don't expose raw set-state API to world your atoms transition from one meanigful to another meaningful state. There is not transional or partially correct state of the atoms.

No orbitrary code is able to modify the state of your atoms.

When you refactor you code or fixing a bug then you are certain you have only single place to make change to or review.

Composing interactions

Quite often you need to perform multiple state changes per single use action. For example, when a user creates a new slide then they expect that:

  • (1) a new slide appears in the list of the slides
  • (2) and new slide becomes selected in the list of the slides

The code to perform these two actions together is actuall, a very simple and familiar function composition:

// file: slide-commands.ts

import { createNewEmptyAfterCurrentSlide } from "@state/slideList"
import { switchToNextSlide } from "@state/slideList"


const createNewSlideAfterCurrentSlide = () => {
  createNewEmptyAfterCurrentSlide();
  switchToNextSlide();
}

Self-documenting functions

A javascript function declaration is a great descriptor of the action it performs. And it is obvious and familiar documentation of how to use it.

When you declare your action as functions you provide

  • descriptive function name
  • descriptive list of parameters
  • descriptive type of parameters
  • optional, js-doc.

You loose these clear descriptions when you are forced to:

  • create lists of string of the name of the actions
  • implement switch/case clauses
  • have "reducer" code
  • wrap actions into hooks code
  • wrap actions into useMemo, useCallback code

Manageable modifications

Application layers

For larger project, mc recommends to split state operation and user actions code into two separate layers.

State actions layer

The state actions are simple javascript functions on top of the state particles.

The state actions code knows nothing about user actions, DOM, UI, HTML and Components.

This layer contains all the business logic of application and it stays easy-to-test.

User interface commands

User interface commands are build on top of state actions.

User interface commands know all about the hovers, mousedowns, clicks, drags, keypresses, onChanges, of your application and make it smooth for users.

This way, all the UI-complications don't get mixed up with the business logic.

For even larger projects it might make sense to move state actions of a specific business domain to a dedicated workspace package.

Derived particles


// word.ts 

const wordCharm = charm("compatibility");
const lettersCharm = derivedCharm(wordCharm, (word) => word.length, NeverSet)

export const useWord = useCharm(wordCharm);
export const setWord = charmSetter(wordCharm)

export const useLetters = useCharm(lettersCharm);


// Counter.tsx

const WordAndLetters = () => {
  const word = useWord();
  const letters = useLetters();

  return <>
    <p>
      <input onChange={e => setWord(e.target.value)}/>
    </p>
    <p>
      Word "{word}" contains {letters} letters.
    </p>
  </>    
}

Split array atoms

import { expect, mock, test } from "bun:test";
import { splitCharm } from "../src/arrays";
import { charm, type Charm } from "../src/charm";

test("charmList does not change when single value changes", () => {
  const arrayCharm = charm([10, 20]);
  const splitArrayCharm = splitCharm(arrayCharm);

  const charm0: Charm<number> = splitArrayCharm.get()[0];
  const charm1: Charm<number> = splitArrayCharm.get()[1];

  const nopFn = () => { }
  const subCharmMock = mock(nopFn as any)
  const sub0 = mock(nopFn as any)
  const sub1 = mock(nopFn as any)

  /// test


  splitArrayCharm.sub(subCharmMock)
  charm0.sub(sub0);
  charm1.sub(sub1);

  charm0.set(0);

  expect(sub0).toHaveBeenCalledTimes(1);

  expect(sub1).toHaveBeenCalledTimes(0);

  expect(subCharmMock).toHaveBeenCalledTimes(0);
});

Using Provider for SSR


const aCharm = charm(1);

const Comp = ({}) => {
  const value = useCharm(aCharm)
  return <button>
    {value}
  </button>
}


test("render two pages at the same time", () => {
  const page1 = execWithCharm(createCharmStore(), () => {
    const comp = <Comp />
    aCharm.set(10);
    return renderToString(comp)
  })
  const page2 = execWithCharm(createCharmStore(), () => {
    const comp = <Comp />
    aCharm.set(20);
    return renderToString(comp)
  })

  expect(page1).toEqual("<button>10</button>")
  expect(page2).toEqual("<button>20</button>")
});

Install


npx jsr add @kaigorod/charm

# or

bunx 

# or

deno add @kaigorod/charm

This command will add the following line to your package.json file

{
  //in package.json, 
  "@kaigorod/charm": "npm:@jsr/kaigorod__charm",
}
import { charm, charmGetter, charmSetter, useCharm } from "@kaigorod/charm";

const isImageSearchOnCharm = charm(false);

export const useIsImageSearchOn = () => useCharm(isImageSearchOnCharm);
export const setIsImageSearchOn = charmSetter(isImageSearchOnCharm);

Inspiration

Charm is inspired by recoil and jotai state management libraries.

Links

  • Github Repo https://github.com/kaigorod/charm
  • Deno Package https://jsr.io/@kaigorod/charm