Skip to content

Latest commit

 

History

54 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-rock

A lightweight, type-safe reactive state store for React and TypeScript.

react-rock provides a simple store for managing structured application data with schema validation, CRUD operations, querying, metadata, subscriptions, and React integration.

The core store is independent of React. React components connect to the store through useStore(), which is built on React's useSyncExternalStore.

Features

  • Type-safe state management
  • Schema-based validation with xanv
  • Reactive updates
  • React integration
  • External subscriptions
  • Scoped subscriptions
  • CRUD operations
  • Bulk creation
  • Query operators
  • Metadata management
  • Row ordering
  • Automatic row identifiers
  • Automatic row version identifiers
  • No React dependency in the core store

Installation

npm install react-rock xanv

Quick Start

Create a store with a schema:

import createStore, { useStore } from "react-rock";
import { xv } from "xanv";

const users = createStore({
  name: xv.string(),
  email: xv.string(),
  age: xv.number(),
});

Use the store inside a React component:

function Users() {
  const store = useStore(users);

  const rows = store.rows;

  return (
    <div>
      {rows.map((user) => (
        <div key={user.rid}>
          {user.name} — {user.email}
        </div>
      ))}
    </div>
  );
}

Create data:

users.create({
  data: {
    name: "John",
    email: "john@example.com",
    age: 25,
  },
});

When the store changes, subscribed React components automatically re-render.


Core Concepts

Store

A Store contains a collection of rows and provides operations for reading, creating, updating, deleting, and reordering those rows.

const users = createStore({
  name: xv.string(),
  email: xv.string(),
  age: xv.number(),
});

The schema defines the shape of every row.

Each row automatically contains two internal fields:

{
  rid: number;
  vid: number;
}

rid is the row identifier.

vid is the row version identifier and changes whenever the row is updated.

A row therefore has a shape similar to:

{
  name: string;
  email: string;
  age: number;
  rid: number;
  vid: number;
}

React Integration

useStore()

useStore() connects a Store to React.

import { useStore } from "react-rock";

function UserList() {
  const store = useStore(users);

  return (
    <ul>
      {store.rows.map((user) => (
        <li key={user.rid}>{user.name}</li>
      ))}
    </ul>
  );
}

Internally, useStore() uses React's useSyncExternalStore.

The store itself does not depend on React, which means it can also be used outside React components.


Subscriptions

subscribe()

Subscribe to store changes from any JavaScript or TypeScript code.

const unsubscribe = users.subscribe(() => {
  console.log("Users changed");
});

Stop listening:

unsubscribe();

Subscriptions are not limited to React components.

For example:

users.subscribe(() => {
  console.log(users.rows);
});

This can be used by services, event handlers, application logic, or other external consumers.


Scoped Subscriptions

react-rock supports subscription scopes through scope.

function User({ id }: { id: number }) {
  const store = useStore(users, `user:${id}`);

  const user = store.findById(id);

  return <div>{user?.name}</div>;
}

A store operation can notify a specific scope:

users.update({
  where: {
    rid: id,
  },
  data: {
    name: "Naxrul",
  },
  scope: `user:${id}`,
});

Only subscribers using that scope are notified.

This allows larger stores to be consumed by independently reactive components.

A global subscription can still be used:

users.subscribe(() => {
  // Global store update
});

Creating Rows

create()

Create a single row.

const user = users.create({
  data: {
    name: "John",
    email: "john@example.com",
    age: 25,
  },
});

The created row is returned:

console.log(user.rid);
console.log(user.vid);

Values are passed through the corresponding schema before being stored.


Bulk Creation

createMany()

Create multiple rows at once.

const created = users.createMany({
  data: [
    {
      name: "John",
      email: "john@example.com",
      age: 25,
    },
    {
      name: "Jane",
      email: "jane@example.com",
      age: 28,
    },
  ],
});

The result is an array containing the created rows.

Notifications can be disabled:

users.createMany({
  data: [
    {
      name: "John",
      email: "john@example.com",
      age: 25,
    },
  ],
  notify: false,
});

Reading Data

rows

Access all rows:

const rows = users.rows;

The rows are returned in their current order.


find()

Find rows using a where condition.

const result = users.find({
  where: {
    name: "John",
  },
});

Multiple conditions are supported:

const result = users.find({
  where: {
    name: "John",
    age: 25,
  },
});

Conditions are evaluated together.


Query Operators

react-rock supports several query operators.

contain

users.find({
  where: {
    name: {
      contain: "oh",
    },
  },
});

Useful for substring matching.

startWith

users.find({
  where: {
    name: {
      startWith: "Jo",
    },
  },
});

endWith

users.find({
  where: {
    name: {
      endWith: "hn",
    },
  },
});

equalWith

users.find({
  where: {
    age: {
      equalWith: 25,
    },
  },
});

notEqualWith

users.find({
  where: {
    age: {
      notEqualWith: 25,
    },
  },
});

gt

Greater than:

users.find({
  where: {
    age: {
      gt: 18,
    },
  },
});

lt

Less than:

users.find({
  where: {
    age: {
      lt: 30,
    },
  },
});

gte

Greater than or equal to:

users.find({
  where: {
    age: {
      gte: 18,
    },
  },
});

lte

Less than or equal to:

users.find({
  where: {
    age: {
      lte: 30,
    },
  },
});

Finding a Single Row

findOne()

Returns the first matching row or null.

const user = users.findOne({
  where: {
    email: "john@example.com",
  },
});

Finding by ID

findById()

Find a row using its rid.

const user = users.findById(1);

The result is either the row or undefined.

if (user) {
  console.log(user.name);
}

Row Index

getIndex()

Get the current position of a row.

const index = users.getIndex(user.rid);

If the row does not exist, -1 is returned.


Updating Rows

update()

Update one or more matching rows.

users.update({
  where: {
    rid: 1,
  },
  data: {
    name: "Naxrul",
  },
});

Multiple rows can be updated:

users.update({
  where: {
    age: {
      lt: 18,
    },
  },
  data: {
    age: 18,
  },
});

rid and vid cannot be modified through data.

When a row is updated, its vid is automatically regenerated.


Deleting Rows

delete()

Delete rows matching a condition.

const count = users.delete({
  where: {
    rid: 1,
  },
});

The return value is the number of deleted rows.

console.log(count);

Multiple rows can be deleted:

users.delete({
  where: {
    age: {
      lt: 18,
    },
  },
});

Reordering Rows

move()

Move a row from one index to another.

users.move({
  fromIndex: 3,
  toIndex: 0,
});

The method returns:

boolean

true means the row was moved successfully.

false means one or both indexes were invalid.


Metadata

Stores can optionally contain metadata in addition to rows.

const users = createStore(
  {
    name: xv.string(),
    email: xv.string(),
  },
  {
    page: xv.number().default(1),
    loading: xv.boolean().default(false),
    search: xv.string().default(""),
  },
);

Metadata is separate from row data.

It is useful for information such as:

  • Pagination state
  • Loading state
  • Search state
  • UI state
  • Filters
  • Application-specific store state

Reading Metadata

getMeta()

const page = users.getMeta("page");

The returned value is strongly typed according to the metadata schema.


Setting Metadata

setMeta()

users.setMeta("page", 2);

Another example:

users.setMeta("loading", true);

Metadata values are validated using their corresponding schema.


Deleting Metadata

deleteMeta()

users.deleteMeta("search");

Clearing Metadata

clearMetas()

Remove all metadata:

users.clearMetas();

Notification Control

Store operations normally notify subscribers automatically.

For operations that support notification control:

users.create({
  data: {
    name: "John",
    email: "john@example.com",
    age: 25,
  },
  notify: false,
});

The following operations support notify:

  • create
  • createMany
  • update
  • delete
  • move

This can be useful when performing multiple mutations and notifying subscribers only when the complete operation is finished.


Type Safety

react-rock derives row types directly from the schema.

For example:

const users = createStore({
  name: xv.string(),
  email: xv.string(),
  age: xv.number(),
});

TypeScript understands:

const user = users.create({
  data: {
    name: "John",
    email: "john@example.com",
    age: 25,
  },
});

Incorrect values are rejected:

users.create({
  data: {
    name: "John",
    email: "john@example.com",
    age: "25",
  },
});

The same schema-derived types are used by:

  • create()
  • createMany()
  • find()
  • findOne()
  • findById()
  • update()
  • delete()
  • getMeta()
  • setMeta()

Optional Fields

Optional fields can be defined using xanv.

const users = createStore({
  name: xv.string(),
  email: xv.string(),
  age: xv.number().optional(),
});

The resulting row type contains an optional age.

{
  name: string;
  email: string;
  age?: number | undefined;
  rid: number;
  vid: number;
}

Example: Complete Store

import createStore, { useStore } from "react-rock";
import { xv } from "xanv";

const users = createStore(
  {
    name: xv.string(),
    email: xv.string(),
    age: xv.number(),
  },
  {
    loading: xv.boolean().default(false),
    search: xv.string().default(""),
  },
);

function UserList() {
  const store = useStore(users);

  const search = store.getMeta("search");

  const rows = store.find({
    where: {
      name: {
        contain: search,
      },
    },
  });

  return (
    <div>
      {rows.map((user) => (
        <div key={user.rid}>
          <strong>{user.name}</strong>
          <span>{user.email}</span>
        </div>
      ))}
    </div>
  );
}

function addUser() {
  users.create({
    data: {
      name: "John",
      email: "john@example.com",
      age: 25,
    },
  });
}

function updateUser(id: number) {
  users.update({
    where: {
      rid: id,
    },
    data: {
      name: "Updated User",
    },
  });
}

function removeUser(id: number) {
  users.delete({
    where: {
      rid: id,
    },
  });
}

API Reference

createStore()

createStore<RS, MS>(
  rowSchema: RS,
  metaSchema?: MS,
): Store<RS, MS>

Creates a new store.


Store

class Store<
  RS extends RowSchema,
  MS extends MetaSchema | undefined = undefined,
>

Properties

rows
metas

Row operations

create()
createMany()
update()
delete()
find()
findOne()
findById()
getIndex()
move()

Metadata operations

getMeta()
setMeta()
deleteMeta()
clearMetas()

Reactive operations

subscribe()
getSnapshot()

API Summary

API Purpose
createStore() Create a store
useStore() Connect a store to React
rows Get all rows
metas Get metadata map
create() Create one row
createMany() Create multiple rows
find() Query rows
findOne() Get first matching row
findById() Find a row by rid
getIndex() Get a row's index
update() Update matching rows
delete() Delete matching rows
move() Reorder rows
getMeta() Read metadata
setMeta() Set metadata
deleteMeta() Delete metadata
clearMetas() Clear metadata
subscribe() Subscribe to changes
getSnapshot() Get the current subscription version

Design Principles

React-independent core

The Store does not use React hooks or React APIs.

React integration is provided separately through useStore().

This allows the same store instance to be used by both React components and ordinary application code.

Schema-driven data

The schema is the source of truth for row and metadata types.

Runtime values are parsed through their corresponding xanv schema.

External-store architecture

The React integration uses useSyncExternalStore, making the store compatible with React's external store subscription model.

Scoped reactivity

scope allows different parts of an application to subscribe to different update scopes without creating separate stores.

Minimal API

The API is intentionally centered around a small set of operations:

create
read
update
delete
subscribe

with querying, metadata, and ordering built around the same store model.


License

Add the project's license information here.

About

A lightweight and powerful state management library for React applications. Simplify global state management with advanced querying, CRUD operations, and selective re-rendering.

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages