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

react-lucy-state

v0.0.6

Published

More performant replacement for React.useState

Downloads

417

Readme

React Lucy State

This library aims to provide a replacement for React.useState primitive which should allow to write more performant code. The library itself is a copy of the state approach from Veles library adapted to React.

How it works

The main problem with React performance is that any state update within a component (or inside a hook you use within a component) will trigger a cascading re-render. Very rarely you want the whole component to re-render, which leads to wasteful building of VDOM and later comparison of render and calculated tree.

This library provides a stable state primitive which never changes, so when you update it, nothing will re-render. While this probably doesn't make sense on its own, it also provides a way to create components which will re-render when the value changes; but you don't need to put it into a separate component and usually you will go for a smallest possible DOM change.

Basic example

Let's see a simple example, a value from input saved to a variable and reflected on the screen:

import { useLucyState } from "react-lucy-state";

function MyComponent() {
  const [inputValue$, setInputValue] = useLucyState("");

  return (
    <div>
      <inputValue$.Value>
        {(value) => (
          <input
            type="text"
            value={value}
            onChange={(e) => setInputValue(e.target.value)}
          />
        )}
      </inputValue$.Value>
      <p>
        Current value is: <inputValue$.Value />
      </p>
      <SomeExpensiveComponent />
    </div>
  );
}

This example will work as you expect if you used regular React.useState, but <MyComponent> itself will never re-render. The only things which will re-render are:

  • <input /> on each update, and only because we need to update an attribute
  • text node which shows the current value

That's it, nothing else will be updated. An important part is that <SomeExpensiveComponent /> won't be re-rendered as well. While it is possible to avoid that re-render by either passing it as a child from a parent component, or exporting the input component into a separate one, with this approach you don't have to, which is increasingly more valuable with more complicated components.

Advanced example

The basic example doesn't look too convincing; as I've described, it is not too hard to extract it to avoid most of the re-renders. Let's build something which is much harder to achieve on your own, a performant list of items:

import { useLucyState, StableIteratorComponent } from "react-lucy-state";

const task1 = { id: 1, content: "first task" };
const task2 = { id: 2, content: "second task" };
const task3 = { id: 3, content: "third task" };
const task4 = { id: 4, content: "fourth task" };
function ListComponent() {
  const tasks$ = useLucyState([task1, task2, task3, task4]);

  return (
    <div>
      <ul>
        <$tasks.Value>
          {(tasks) =>
            tasks.map((task, index) => (
              <StableIteratorComponent key={task.id} item={task} index={index}>
                {(task$, index$) => (
                  <ItemComponent task$={task$} index$={index$} />
                )}
              </StableIteratorComponent>
            ))
          }
        </$tasks.Value>
      </ul>
    </div>
  );
}

function TaskComponent({ task$, index$ }) {
  return (
    <li>
      <index$.Value />: <task$.Value selector={(task) => task.content} />
    </li>
  );
}

This examples introduces a lot of concepts. First, we can see that when we use <tasks$.Value /> component, the callback is not "stable", meaning that it will be re-rendered every time the value inside changes. Usually that's what we want, if we go for smaller DOM changes, but in case of array, that'd be too wasteful (although this is what happens in normal React code). So we wrap each item in <StableIteratorComponent>, which accepts item and index. Inside it wraps them into a Lucy state, and then never re-renders (hence "stable").

The next concept is passing down Lucy state as a property. As I've mentioned, the state object itself will never change, meaning that even if the value does change, the component will not re-render. This means that when a task does change, not the whole component will be re-rendered.

Last concept is selector property on <task$.Value /> component. This improves things even further by allowing you to subscribe to only parts of the object, or you can combine multiple properties; as long as you return a primitive data type, it won't be re-rendered unless it changes.

DOM Attributes

DOM attributes are a major issue for React performance. Often you might need to dynamically assign a class based on some variable, but the DOM Node is somewhere in the middle of the component and can't be extracted easily. There is a not so elegant solution in this library:

import {
  useLucyState,
  UnstableComponent,
  StableComponent,
  useSelect$,
} from "react-lucy-state";

function Component() {
  const [input$, setInput] = useLucyState("");
  // we check if the value contains only numbers
  const hasError$ = useSelect$(input$, (value) => !value.match(/^[0-9]+$/i));

  return (
    <div>
      <UnstableComponent items$={[hasError$]}>
        {([hasError]) => (
          <div className={hasError ? "error" : undefined}>
            <StableComponent>
              <Content hasError$={hasError$} />
            </StableComponent>
          </div>
        )}
      </UnstableComponent>

      <input$.Value>
        {(value) => (
          <input
            type="text"
            value={value}
            onChange={(e) => setInput(e.target.value)}
          />
        )}
      </input$.Value>
    </div>
  );
}

Effects

LucyState provides a useTrackValue and useTrackValueSelector methods, which should work as a replacement for React.useEffect the vast majority of the time. As a simple example:

import { useLucyState } from "react-lucy-state";

function Component() {
  const [counter$, setCounter] = useLucyState(0);

  counter$.useTrackValue((counterValue) => {
    console.log(`counter value is ${counterValue}`);
  });

  return (
    <div>
      <button onClick={() => setCounter((value) => value + 1)}>
        Increment counter
      </button>
    </div>
  );
}

Similar to React.useEffect, you can return a function, which will be executed when the value changes or the component unmounts. If you need to run a function which depends on multiple Lucy states, there is a helper useCombine$:

import { useLucyState, useCombine$ } from "react-lucy-state";

function Component() {
  const [firstCounter$, setFirstCounter] = useLucyState(0);
  const [secondCounter$, setSecondCounter] = useLucyState(0);

  const combinedCounter$ = useCombine$(firstCounter$, secondCounter$);
  combinedCounter$.useTrackValue(([firstValue, secondValue]) => {
    console.log(`total counter value is ${firstValue + secondValue}`);
  });

  return (
    <div>
      <button onClick={() => setFirstCounter(firstCounter$.getValue() + 1)}>
        Increment first counter
      </button>
      <button onClick={() => setSecondCounter(secondCounter$.getValue() + 1)}>
        Increment second counter
      </button>
    </div>
  );
}

Interoperability

You can switch back and forth between regular props and Lucy state, but you should be careful while doing so. First, you can convert a Lucy state into a regular React state property with useConvertLucyStateToProperty helper. Remember that by doing so, the whole component will re-render, including all its children. Here is an example:

function Component() {
  const [value$, setValue] = useLucyState(0);

  return (
    <div>
      <button onClick={() => setValue((value) => value + 1)}>
        Increment value
      </button>
      <Content value$={value$} />
    </div>
  );
}

function Content({ value$ }: { value$: LucyState<number> }) {
  const value = useConvertLucyStateToProperty(value$);

  return <h2>Current value is {value}</h2>;
}

In this example, the whole <Content /> component will re-render on each value change.

You can perform the same operation, but the other way. To do, just wrap a regular React variable in useConvertToLucyState, and you'll receive a stable Lucy state. Again, be careful and remember that just doing so won't change much, as having a regular variable means the whole component will re-render. So you'll probably want to wrap at least some children in a <StableComponent />. Here is a reverse example:

function Component({ value, onClick }) {
  const value$ = useConvertToLucyState(value);

  return (
    <div>
      <button onClick={onClick}>Increment value</button>
      <StableComponent>
        <Content value$={value$} />
      </StableComponent>
    </div>
  );
}

function Content({ value$ }: { value$: LucyState<number> }) {
  useEffect(spy);

  return (
    <h2>
      Current value is <value$.Value />
    </h2>
  );
}

As you can see, we need to wrap <Content /> in a stable component to make sure it doesn't re-render when the value changes.