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

memotable

v3.7.0

Published

Reactive, deeply-indexable, sortable and memoizable maps

Readme

memotable

npm version Bundle size CI License: MIT Try it live

Reactive, deeply-indexable, sortable and memoizable maps — all in <1 KB.
Written in TypeScript with full type definitions. Side-effects free.

The correct way to memoize indexed and ordered keyed-collections.

Most web apps don’t need collection memoization. The additional code complexity is not worth it. The DOM is almost always the real bottleneck for performance.
That said, when you are processing huge amounts of data (e.g. a realtime dashboard or a fully-offline app), memotable gives you the correct memoizable primitive.

Why memotable?

When writing React code, most developers reach for useMemo to cache filtered or sorted collections - but that pattern is a subtle trap.

function TaskList({ tasks, filter, comparator }) {
  // ❌ Looks efficient, but isn't.
  const filtered = useMemo(() => tasks.filter(filter), [tasks, filter]);
  const sorted = useMemo(() => filtered.sort(comparator), [filtered, comparator]);
  ...
}

This has two fundamental problems:

  • If you mutate the array in place, useMemo can silently return stale data because the reference didn’t change.
  • If you recreate the array on every render, you pay full recomputation cost every time — so your “optimization” does nothing.

Most of the time, you don’t need to “memoize” collections at all — just recompute them and move on. But when you do need to avoid recomputation — say, thousands of values with heavy indexing/comparator logic — you need a structure that’s actually designed for that.

That’s what memotable is.

It provides:

  • Indexing — Index collections as deeply as needed.
  • Sorting — Sort at the root or any child node - applies recursively from any node to its children.
  • Subscriptions — Subscribe only to the specific partition you are interested in, ignoring other changes.

💡 You can think of memotable as a utility that lets you shape your data into a render-ready form, and then keeps that shape up to date as edits come in. It also enables you to memoize specific partitions that are frequently read for optimal performance.

Benefits:

  • Lighter render passes – Heavy operations like sorting and indexing are applied outside the render loop.
  • Less re-renders – A table partition notifies subscribers only when it sees any change.

Comparison with vanilla implementation

Sample todo app with filtering and sorting, setup using vanilla JS (write-friendly data structure)-

// Simple map holding all todo's
const todos = new Map<string, ITodo>();

// Generic function to get todo's that match any filter criteria
function getTodos(group: string): ITodo[] {
    return Array.from(todos.values())
        .filter(
            (todo) =>
                (group === "Important" ? todo.isImportant : todo.listId === group) && // Matches group filter
                todo.title.includes(KEYWORD), // Matches keyword filter
        )
        .sort(
            (a, b) =>
                Number(b.isImportant) - Number(a.isImportant) ||
                a.createdDate.getTime() - b.createdDate.getTime(),
        );
}

// Reading specific groups
getTodos("list1"); // Get todo's in "list1"
getTodos("Important"); // Get important todo's

// Update a todo
todo.set("1", { title: "Updated title" });

Identical app setup using memotable (read-friendly data structure)-

// Table of todos
const todos = new Table<string, ITodo>();

// Register partition index
todos.index(
    (todo) => [todo.listId, todo.isImportant ? "Important" : null], // Specify which all top-level partitions a todo belongs to
    (p) => {
        // Create a sub-partition to contain filtered items
        p.index((todo) => (todo.title.includes(KEYWORD) ? "Filtered" : null));
        p.sort(
            (a, b) =>
                Number(b.isImportant) - Number(a.isImportant) ||
                a.createdDate.getTime() - b.createdDate.getTime(),
        );
    },
);

// Reading specific partitions
todos.partition("list1").partition("Filtered"); // Get sorted & filtered todo's in "list1"
todos.partition("Important").partition("Filtered"); // Get sorted & filtered important todo's

// Update a todo (identical to vanilla)
todo.set("1", { title: "Updated title" });

// Whenever keyword updates for a visible partition OR when a new partition becomes visible-
partition.index(); // Ensures latest keyword filter is applied
partition.memo(); // Ensure that the results are memoized for fast reads

Semantics

It's important to understand the semantics of memotable so that you can come up with the most optimal setup for your scenario. Here are the core semantics-

  1. A Table can be recursively split into derived readonly copies or subsets of itself using index method.
  2. Edits (set / delete / touch) can only be applied to the root node and the propagate to all derived nodes.
  3. sort / memo can be independently applied to any node and they propagate to all derived nodes.

Using memotable

Simple indexing and sorting in a React component

const table = new Table<string, Task>();
table.sort((task1, task2) => task1.title.localeCompare(task2.title)); // ✅ Comparator applied at root
table.index((task) => task.listId); // ✅ List index creates a partition for every listId

// ✅ Generic React component that renders a task list
function TaskList({ listId }) {
    useTable(table.partition(listId)); // ✅ Subscription that is only notified when this table gets updated
    return (
        <div>
            {Array.from(table, ([id, task]) => (
                <Task key={id} task={task} />
            ))}
        </div>
    );
}

// Render lists (✅ mounted/visible list gets memoized automatically enabling efficient render passes)
<TaskList listId={"list1"} />;
<TaskList listId={"list2"} />;

// Update task table
taskTable.set("1", { listId: "list1", title: "Task" }); // only re-renders "list1" node

Live Demo

Check out this example — a demo showing how heavy data processing apps can achieve better interactivity using memotable.

Run it locally:

git clone https://github.com/shudv/memotable.git
cd memotable
pnpm install
pnpm demo

Demo GIF

When should you use memotable?

You don't need it for simple apps.

✅ Use it when:

  • Your data set is large enough that filtering/sorting frequently can cause visible frame drops (~10ms+). (typically heavy realtime dashboards OR fully-offline apps with a lot of user data) AND
  • Reads outnumber writes by at least 2-3x.

When not to use memotable

🚫 Avoid it when:

  • Your data set is small enough that plain .filter()/.sort() in a render pass is super fast (say <1ms) OR the number of render passes itself are naturally low enough.
  • The complexity of maintaining derived views correctly outweighs the performance gain.
  • Your data set is so huge that even a single sort/filter pass is noticeably janky (memotable reduces sort/filter passes but does not eliminate them entirely). At that point, consider using a web worker for heavy computation or re-design your app to not require heavy data processing on the client.

What memotable is not

It's not a full state management system like MobX or Zustand. Instead, it's a data structure primitive — designed to integrate with those systems or stand alone for efficient in-memory computation.

Benchmarks

Memotable is optimized for read-heavy workloads. The tradeoff: slower writes, faster reads.

Real-world benchmark

Scenario: 50 lists with 1000 tasks per list with list-based indexing, importance filtering, and two-factor sorting (importance + timestamp). Simulates a typical task management app with 800 reads and 200 writes.

| Operation | vanilla | memotable | Difference | | ------------ | ----------- | ---------- | --------------- | | Initial load | 3.0ms | 35.0ms | | | 200 edits | 0.0ms | 30.3ms | | | 800 reads | 244.7ms | 3.7ms | | | Total | 247.8ms | 68.9ms | 3.6x faster |

Run pnpm benchmark to test on your machine.

Integrations

Memotable is designed to integrate seamlessly with existing tools:

React

import { useTable } from "memotable/react";

function MyComponent({ table }) {
    useTable(table); // Auto-subscribes, memoizes the data, triggers re-render on change, and cleans up on unmount
    return <div>{table.size()} values</div>;
}

Vue / Svelte (WIP)

Coming soon

API Reference

Table

The main Table class provides all the functionality for managing indexed, sorted, and memoized collections.

Constructor

new Table<K, V>();

Creates a new table with key type K and value type V.

Basic Operations

  • set(key: K, value: V): this - Add or update a value
  • get(key: K): V | undefined - Get a value by key
  • has(key: K): boolean - Check if a key exists
  • delete(key: K): boolean - Remove a value by key
  • clear(): void - Remove all values and reset indexing/sorting
  • size: number - Get the number of values in the table

Iteration

  • keys(): MapIterator<K> - Iterate over keys (respects sorting if enabled)
  • values(): MapIterator<V> - Iterate over values (respects sorting if enabled)
  • entries(): MapIterator<[K, V]> - Iterate over key-value pairs

Indexing

  • index(definition: (value: V) => string | string[] | null, partitionInitializer?: (name: string, partition: IReadonlyTable<K, V>) => void): void - Create partitions based on a definition
  • index(null): void - Remove indexing
  • index(): void - Re-index based on existing definition (no-op if no definition provided before)
  • partition(name: string): IReadonlyTable<K, V> - Get a specific partition
  • partition(): IReadonlyTable<K, V> - Get the default partition
  • partitions(): string[] - Get all partition names (includes empty partitions)

Sorting

  • sort(comparator: (a: V, b: V) => number): void - Set a comparator function
  • sort(null): void - Remove sorting
  • sort(): void - Re-sort based on existing comparator (no-op if no comparator applied before)

Memoization

  • memo(flag?: boolean): void - Enable or disable memoization (default: true)

Subscriptions

  • subscribe(subscriber: (keys: K[]) => void): () => void - Subscribe to changes. Returns an unsubscribe function.

Batching

  • batch(fn: (t: TBatchable<K, V>) => void): void - Group multiple operations into a single update

Advanced

  • touch(key: K): void - Mark a value as changed without replacing it (useful when the value is mutated in place OR indexing/sorting logic changes in a way that affects a key)

React Integration

  • useTable(table: IReadonlyTable<K, V>): void - React hook that subscribes to table changes and triggers re-renders

License

MIT

Next steps: