npm package: @lazylab/react-use-stable-state
Description: A hook that only updates state if the value has actually changed, with optional deep compare support.
Latest version: 1.0.0

# @lazylab/react-use-stable-state

[![NPM Downloads](https://img.shields.io/npm/dt/@lazylab/react-use-stable-state.svg)](https://www.npmjs.com/package/@lazylab/react-use-stable-state)
[![NPM Version](https://img.shields.io/npm/v/@lazylab/react-use-stable-state.svg)](https://www.npmjs.com/package/@lazylab/react-use-stable-state)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A React hook that only updates state if the value has actually changed, with optional custom comparison support (like deep or shallow equality).

## The Problem

A classic issue in React is triggering unnecessary re-renders when you update state with an object or an array that is structurally identical, but has a new reference:

```tsx
// This triggers a re-render even though the content is exactly the same!
setState({ page: 1 })
```

Or when you receive data from an API where the content is unchanged but the object reference is new. React compares state by reference (`Object.is`), not by structure.

## The Solution

`useStableState` provides a minimal, predictable wrapper around `useState` that allows you to specify exactly how the old and new state should be compared.

### 🆚 `useState` vs `useStableState`

| Feature | `useState` | `useStableState` |
|---------|-----------|------------------|
| **Comparison Method** | Fixed to `Object.is` (Reference equality) | **Pluggable** (Default: `shallowEqual`, supports Deep/Reference) |
| **Object Updates** | Re-renders if reference changes (even if identical) | **Prevents re-render** if content is equal |
| **API Responses** | Triggers re-render on identical polled data | **Safely ignores** unchanged data |
| **Learning Curve** | Standard React | **Zero** (Same API as `useState`) |
| **Bundle Size** | Built-in | **< 1kb** (Zero dependencies) |

## Installation

```bash
npm install @lazylab/react-use-stable-state
# or
yarn add @lazylab/react-use-stable-state
# or
pnpm add @lazylab/react-use-stable-state
```

## Usage

### Basic Usage (Default: `shallowEqual`)

By default, `useStableState` uses a built-in `shallowEqual` comparison. This means if you pass an object with the exact same top-level properties, it will **not** trigger a re-render:

```tsx
import { useStableState } from '@lazylab/react-use-stable-state';

function Filters() {
  const [filters, setFilters] = useStableState({ page: 1, sort: 'asc' });

  const handleUpdate = () => {
    // This will NOT trigger a re-render because it is shallowly equal!
    setFilters({ page: 1, sort: 'asc' }); 
  };

  return <button onClick={handleUpdate}>Update Filters</button>;
}
```

### With Reference Equality (Like standard `useState`)

If you want the exact same behavior as `useState` (comparing by reference), you can pass `Object.is` as the compare function:

```tsx
import { useStableState } from '@lazylab/react-use-stable-state';

function App() {
  const [count, setCount] = useStableState(0, { compare: Object.is });
}
```

### With Deep Comparison (e.g., `fast-deep-equal` or `lodash.isEqual`)

If you have deeply nested objects (like complex API responses), you can inject any comparison library you want:

```tsx
import { useStableState } from '@lazylab/react-use-stable-state';
import deepEqual from 'fast-deep-equal';

function DataView({ initialData }) {
  const [data, setData] = useStableState(initialData, {
    compare: deepEqual
  });

  const handleNewData = (newData) => {
    // Only re-renders if the actual deep content changed
    setData(newData);
  };
}
```

## API

### `useStableState(initialValue, options?)`

#### Parameters

- `initialValue`: The initial state value (or a lazy initializer function `() => initialValue`).
- `options` (optional):
  - `compare`: A function `(prev: T, next: T) => boolean` that returns `true` if the values should be considered equal. Defaults to `Object.is`.

#### Returns

Returns a tuple `[state, setState]` exactly like standard `useState`. `setState` supports both direct values and functional updates `(prev => next)`.

### `shallowEqual(a, b)`

A microscopic utility function that performs a shallow comparison of two objects. It returns `true` if they have the same keys with the same top-level values (compared using `Object.is`).

## Philosophy

We didn't want to:
- Reinvent a state manager
- Create magical proxies
- Force heavy comparison libraries on you

We wanted:
- A predictable, explicit wrapper
- A hook that solves the origin of the problem (`setState`) rather than patching the symptoms (e.g. `useDeepCompareEffect`)

## License

MIT