valtio-reactive
Version:
valtio-reactive makes Valtio a reactive library
187 lines (135 loc) • 4.83 kB
Markdown
# valtio-reactive
[](https://github.com/valtiojs/valtio-reactive/actions?query=workflow%3ACI)
[](https://www.npmjs.com/package/valtio-reactive)
[](https://bundlephobia.com/result?p=valtio-reactive)
[](https://discord.gg/MrQdmzd)
Reactive primitives for [valtio](https://github.com/pmndrs/valtio) — adds, `computed`, `effect`, and `batch` to enable fine-grained reactivity outside of React.
## Motivation
`valtio`'s reactive capabilities are primarily designed for React via `useSnapshot` and only has limited support for computed values. `valtio-reactive` was made to fill those gaps while keeping `valtio` lean and fast.
- Run side effects when specific properties change (not just any change)
- Create derived/computed state that automatically updates
- Batch multiple updates into a single reaction
See the [original discussion](https://github.com/pmndrs/valtio/discussions/949) for more context.
## Installation
```bash
npm install valtio valtio-reactive
```
## API
### `effect(fn, cleanup?): Dispose
This runs the first function (`fn`) immediately and re-runs it whenever any of the properties that are accessed in that function change. Only the properties are actually read during execution are tracked — changes to unread properties won't trigger re-runs. It returns a `dispose` function that will run the cleanup function when called.
```ts
import { proxy } from 'valtio/vanilla';
import { effect } from 'valtio-reactive';
const state = proxy({
count: 0,
unrelated: 'hello'
user: {
settings: {
theme: 'light' //
},
name: 'Bob'
},
})
const dispose = effect(
() => {
console.log('count is: ', state.count)
console.log('theme is: ', state.user.settings.theme)
},
() => {
// optional cleanup function
console.log('cleaning up')
}
)
// immediately logs:
// "count is: 0"
// "theme is: light'
state.count++
// logs:
// "count is: 1"
// "theme is: light"
state.unrelated = 'world' // nothing happens when this property is changed because it wasn't accessed
state.user.name = 'Robert' // nothing happens
state.user.settings.theme = 'dark'
// logs:
// "count is: 1"
// "theme is: dark"
dispose()
// logs "cleaning up"
```
---
### `batch(fn): T`
Batches multiple state changes so that effects only react once after all changes complete. Returns the value returned by `fn`.
```ts
import { proxy } from 'valtio/vanilla';
import { batch, effect } from 'valtio-reactive';
const state = proxy({ count: 0 });
effect(() => {
console.log('count:', state.count);
});
// Logs: "count: 0"
batch(() => {
state.count++;
state.count++;
state.count++;
});
// Logs: "count: 3" (only once, not three times)
```
---
### `computed(obj): T`
Creates a proxy object with computed/derived properties. Each property is defined as a getter function that automatically re-runs when its dependencies change.
```ts
import { proxy } from 'valtio/vanilla';
import { computed } from 'valtio-reactive';
const state = proxy({ count: 1 });
const derived = computed({
double: () => state.count * 2,
quadruple: () => state.count * 4,
});
console.log(derived.double); // 2
console.log(derived.quadruple); // 4
state.count = 5;
console.log(derived.double); // 10
console.log(derived.quadruple); // 20
```
The returned object is itself a `valtio` proxy, so you can use it with `effect`, `useSnapshot`, or any other `valtio` utility.
---
## Usage with React
While these primitives are framework-agnostic, they integrate seamlessly with `valtio`'s React bindings:
```tsx
import { proxy, useSnapshot } from 'valtio';
import { effect, computed } from 'valtio-reactive';
const state = proxy({ count: 0 });
// Computed values work with useSnapshot
const derived = computed({
double: () => state.count * 2,
});
// Side effects outside of React
effect(() => {
console.log('Count changed:', state.count);
});
function Counter() {
const snap = useSnapshot(state);
const derivedSnap = useSnapshot(derived);
return (
<div>
<p>Count: {snap.count}</p>
<p>Double: {derivedSnap.double}</p>
<button onClick={() => state.count++}>+1</button>
</div>
);
}
```
---
## TypeScript
All exports are fully typed. The `computed` function infers types from your getter functions:
```ts
const state = proxy({ count: 1, name: 'test' });
const derived = computed({
double: () => state.count * 2, // inferred as number
message: () => `Hello ${state.name}`, // inferred as string
});
derived.double; // number
derived.message; // string
```
## License
MIT