npm package: @notyetofficially/upkeep
Description: A lightweight TypeScript library for **mutable**, **proxy‑based** state management.
Latest version: 0.2.0

# UpKeep

A lightweight TypeScript library for **mutable**, **proxy‑based** state management.

- make any object/array/Map/Set reactive
- shallow by default, you opt-in to nested reactivity
- subscribe to changes
- batch updates (unless precise subscriptions)
- binding for React

You’ll find interactive examples here:

- React playground: **(StackBlitz link here)**
- Vanilla playground: **(StackBlitz link here)**

## Installation

```bash
bun add @notyetofficially/upkeep
```
```bash
npm install @notyetofficially/upkeep
```
```bash
yarn add @notyetofficially/upkeep
```
```bash 
pnpm add @notyetofficially/upkeep
```


## Quick start

### 1. Make something reactive

```ts
import { monitor, subscribe } from "@notyetofficially/upkeep";

const counter = monitor({ value: 0 });

// Listen to changes
const stop = subscribe(() => {
	console.log("count is: ", counter.value); // it will run first time to figure out dependencies, logs: "count is: 0"
});

// somewhere in code
counter.count += 1;
counter.count += 1; // because its batched, logs: "count is 2"

// stop(); // stop tracking when you want to cleanup.
```

### 2. Reactivity is **shallow**: nested values only become reactive when you explicitly wrap them with `monitor`.

```ts
import { monitor, Monitored, subscribe } from "@notyetofficially/upkeep";

class Example extends Monitored {
  value = 1;
  regularList: number[] = [];
  reactiveList = monitor<number[]>([]);
}

const example = new Example(); // you don't need to wrap with monitor(...), because it inherits Monitored class.

subscribe(() => {
  console.log("value =", example.value, "reactive list length =", example.reactiveList.length);
});

example.value = 2;           
example.reactiveList.push(1, 2, 3);

// example.regularList.push(1); // NO effect: regularList won't trigger subscribe's callback
```

This makes it explicit which parts of your model are “live”.

## React usage

The React integration is built on top of the same core primitives.  
The main hook is `useProxy`.

### 1. Basic example

```tsx
import React from "react";
import { monitor } from "@notyetofficially/upkeep";
import { useProxy } from "@notyetofficially/upkeep/react";

class Counter {
  count = 0;
}

const counter = monitor(new Counter());

export function CounterView() {
  // tracking proxy – only properties read during render become dependencies
  const state = useProxy(counter);

  return (
    <div>
      <button onClick={() => (counter.count -= 1)}>-</button>
      <span>{state.count}</span>
      <button onClick={() => (counter.count += 1)}>+</button>
    </div>
  );
}
```

How it works:

- `useProxy(counter)` returns a **tracking proxy**.
- When React renders and reads `state.count`, the hook subscribes to that field.
- Changing `counter.count` later will cause only this component to re‑render.
- Other fields on `counter` do **not** trigger this component unless they are read during render.

> Note: you can directly manipulate the state, no need to remember should you update monitored object or state (both works).

### 2. Nested example

You often want:

- The list component to re‑render when the **list structure** changes (`push`, `splice`, …).
- Each item component to re‑render only when **its own fields** change.


```tsx
import React, { useState } from 'react';
import { monitor } from "@notyetofficially/upkeep";
import { useProxy, } from "@notyetofficially/upkeep/react";

type Todo = { id: number; text: string };

let todoId = 0;

class Todos {
  list: Todo[] = monitor([
    { id: todoId++, text: 'First' },
    { id: todoId++, text: 'Second' },
    { id: todoId++, text: 'Fifth' },
  ]);

  remove(id: number) {
    const index = this.list.findIndex((x) => x.id === id);
    if (index !== -1) this.list.splice(index, 1);
  }
  create(text: string) {
    this.list.push({ id: todoId++, text });
  }
}

const todos = monitor(new Todos());

function RenderTodoItem(props: { todo: Todo; onRemove: (todo: Todo) => void }) {
  const state = useProxy(props.todo); // always keep on top

  return (
    <li>
      {state.id} - {state.text} -{' '}
      <button onClick={() => props.onRemove(state)}>x</button>
    </li>
  );
}
function RenderTodos() {
  const state = useProxy(todos);
  const [text, setText] = useState('');

  const onRemove = (todo: Todo) => {
    todos.remove(todo.id);
    // state.remove(todo.id); // both work
  };
  const onCreate = () => {
    todos.create(text);
    setText('');
  };

  return (
    <>
      <ul>
        {state.list.map((todo) => {
          // This is more of demonstration
          return (
            <RenderTodoItem key={todo.id} todo={todo} onRemove={onRemove} />
          );
        })}
      </ul>
      <hr />
      <input
        type="text"
        value={text}
        onInput={(ev) => setText(ev.currentTarget.value)}
      />
      <button disabled={!text.trim()} onClick={onCreate}>
        create
      </button>
    </>
  );
}

```

> For more examples please check out `tests` folder

## License

MIT