Skip to content

Repository files navigation

React BucketJS

npm version

Ultra-lightweight state management for React. Create a bucket to hold any value, update it from anywhere, and components that use it re-render automatically.

  • Works with primitives, arrays, and objects
  • Fine-grained re-render control via selectors and comparators
  • No dependencies — only React 18+ as a peer dependency
npm install react-bucketjs

Core concept

A bucket is a tiny store you create outside of any component. Export it from a shared file and import it wherever you need it — no providers, no context.

// store.js
import { bucket } from "react-bucketjs";

// TypeScript infers the type from the initial value
export const btCount = bucket(0);            // BucketType<number>

// Or provide the generic explicitly
export const btCount = bucket<number>(0);

// Useful when the type is wider than the initial value
type Status = "idle" | "loading" | "error";
export const btStatus = bucket<Status>("idle");

// Objects work the same way
type User = { name: string; age: number };
export const btUser = bucket<User>({ name: "Alice", age: 30 });
// Counter.jsx
import { useBucket } from "react-bucketjs";
import { btCount } from "./store";

function Counter() {
    const count = useBucket(btCount);

    return (
        <div>
            <h1>{count}</h1>
            <button onClick={() => btCount.set(count + 1)}>Increment</button>
        </div>
    );
}

Hooks

useBucket<T>(bucket, comparator?)

Subscribe to the entire bucket state. Re-renders the component whenever the state changes.

// Type is inferred from the bucket
const count = useBucket(btCount);             // number | undefined

// Or provide it explicitly
const count = useBucket<number>(btCount);     // number | undefined

useBucketSelector<T, R>(bucket, selector, comparator?)

Subscribe to a single derived value. The component only re-renders when that specific value changes — other fields in the bucket are ignored.

T is the bucket state type, R is the type returned by the selector.

type User = { name: string; age: number };
const btUser = bucket<User>({ name: "Alice", age: 30 });

function Name() {
    // T and R are both inferred from the bucket and selector
    const name = useBucketSelector(btUser, (s) => s.name);           // string

    // Or provide them explicitly
    const name = useBucketSelector<User, string>(btUser, (s) => s.name);

    return <span>{name}</span>;
}

useBucketSelectorX<T, R>(bucket, selector, comparator?)

Same as useBucketSelector but returns a [value] tuple. Useful when you want to destructure a single value in a way that is easy to extend later.

// Inferred
const [name] = useBucketSelectorX(btUser, (s) => s.name);            // [string]

// Explicit
const [name] = useBucketSelectorX<User, string>(btUser, (s) => s.name);

useBuckets(buckets[], comparator?)

Subscribe to multiple buckets at once. Returns an array of their current state values, in the same order as the input array. Re-renders whenever any of them changes.

The return type is inferred as a plain any[], so explicitly typing your buckets is the recommended way to keep things type-safe.

const btA = bucket<number>(1);
const btB = bucket<string>("hello");

function Display() {
    const [a, b] = useBuckets([btA, btB]); // [number, string]
    return <p>{a}  {b}</p>;
}

Bucket methods

Once you have a bucket you can read and write its state from anywhere — inside or outside of React components.

Method Description
bucket.get(selector?) Read the current state. Pass an optional selector to read a derived value.
bucket.set(value | updater) Replace the state. Pass a function to derive the next value from the current one.
bucket.cset(value | updater) Same as set but deep-clones the value before storing it.
bucket.assign(partial) Shallow-merge a partial object into the current state (like Object.assign).
bucket.cassign(partial) Same as assign but deep-clones the partial before merging.
bucket.copy(selector?) Returns a deep clone of the state (or a selected slice).
bucket.subscribe(fn) Register a listener called on every change. Returns an unsubscribe function.
bucket.emit() Manually notify all subscribers without changing the state.

set with an updater function

const btCount = bucket(0);

btCount.set((current) => current + 1); // 1
btCount.set((current) => current + 1); // 2

assign for partial object updates

const btUser = bucket({ name: "Alice", age: 30 });

btUser.assign({ age: 31 });
// state is now { name: "Alice", age: 31 }

Comparators

By default, re-renders are triggered by reference equality (===). Pass a comparator to take control.

A comparator receives the previous and next value. Return true to skip the re-render, false to allow it.

import { useBucket, compareStringified } from "react-bucketjs";

// Re-render only when the `role` field changes
const byRole = (prev, next) => prev.role === next.role;
const user = useBucket(btUser, byRole);

// Re-render based on deep equality (JSON comparison)
const value = useBucket(btData, compareStringified);

compareStringified is a built-in helper that returns true when two values serialize to the same JSON string (i.e. they are deeply equal), suppressing the re-render.


Subscribing outside React

Use bucket.subscribe to react to changes in non-component code such as logging, analytics, or synchronisation.

Note: the subscriber receives the internal store wrapper { state: T }, so access your value via store.state.

import { bucket } from "react-bucketjs";

const btCounter = bucket(0);

const unsubscribe = btCounter.subscribe((store) => {
    console.log("Count:", store.state);
});

btCounter.set(1); // Count: 1
btCounter.set(2); // Count: 2

unsubscribe(); // stop listening

About

React state-management using elegant buckets with hooks to support re-render on changes.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages