|
5 | 5 | [](https://bundlephobia.com/result?p=valtio-reactive) |
6 | 6 | [](https://discord.gg/MrQdmzd) |
7 | 7 |
|
8 | | -valtio-reactive makes Valtio a reactive library |
| 8 | +Reactive primitives for [valtio](https://github.com/pmndrs/valtio) — adds, `computed`, `effect`, and `batch` to enable fine-grained reactivity outside of React. |
9 | 9 |
|
10 | | -## Background |
| 10 | +## Motivation |
11 | 11 |
|
12 | | -See: https://github.com/pmndrs/valtio/discussions/949 |
| 12 | +`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. |
13 | 13 |
|
14 | | -## Install |
| 14 | +- Run side effects when specific properties change (not just any change) |
| 15 | +- Create derived/computed state that automatically updates |
| 16 | +- Batch multiple updates into a single reaction |
| 17 | + |
| 18 | +See the [original discussion](https://github.com/pmndrs/valtio/discussions/949) for more context. |
| 19 | + |
| 20 | +## Installation |
15 | 21 |
|
16 | 22 | ```bash |
17 | 23 | npm install valtio valtio-reactive |
18 | 24 | ``` |
19 | 25 |
|
20 | | -## Usage |
| 26 | +## API |
| 27 | + |
| 28 | +### `effect(fn, cleanup?): Dispose |
| 29 | + |
| 30 | +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. |
| 31 | + |
| 32 | +```ts |
| 33 | +import { proxy } from 'valtio/vanilla'; |
| 34 | +import { effect } from 'valtio-reactive'; |
| 35 | + |
| 36 | +const state = proxy({ |
| 37 | + count: 0, |
| 38 | + unrelated: 'hello' |
| 39 | + user: { |
| 40 | + settings: { |
| 41 | + theme: 'light' // |
| 42 | + }, |
| 43 | + name: 'Bob' |
| 44 | + }, |
| 45 | + |
| 46 | +}) |
| 47 | + |
| 48 | +const dispose = effect( |
| 49 | + () => { |
| 50 | + console.log('count is: ', state.count) |
| 51 | + console.log('theme is: ', state.user.settings.theme) |
| 52 | + }, |
| 53 | + () => { |
| 54 | + // optional cleanup function |
| 55 | + console.log('cleaning up') |
| 56 | + } |
| 57 | +) |
| 58 | +// immediately logs: |
| 59 | +// "count is: 0" |
| 60 | +// "theme is: light' |
| 61 | +state.count++ |
| 62 | +// logs: |
| 63 | +// "count is: 1" |
| 64 | +// "theme is: light" |
| 65 | +state.unrelated = 'world' // nothing happens when this property is changed because it wasn't accessed |
| 66 | +state.user.name = 'Robert' // nothing happens |
| 67 | + |
| 68 | +state.user.settings.theme = 'dark' |
| 69 | +// logs: |
| 70 | +// "count is: 1" |
| 71 | +// "theme is: dark" |
| 72 | + |
| 73 | +dispose() |
| 74 | +// logs "cleaning up" |
| 75 | +``` |
| 76 | + |
| 77 | +--- |
| 78 | + |
| 79 | +### `batch(fn): T` |
| 80 | + |
| 81 | +Batches multiple state changes so that effects only react once after all changes complete. Returns the value returned by `fn`. |
| 82 | + |
| 83 | +```ts |
| 84 | +import { proxy } from 'valtio/vanilla'; |
| 85 | +import { batch, effect } from 'valtio-reactive'; |
| 86 | + |
| 87 | +const state = proxy({ count: 0 }); |
| 88 | + |
| 89 | +effect(() => { |
| 90 | + console.log('count:', state.count); |
| 91 | +}); |
| 92 | +// Logs: "count: 0" |
| 93 | + |
| 94 | +batch(() => { |
| 95 | + state.count++; |
| 96 | + state.count++; |
| 97 | + state.count++; |
| 98 | +}); |
| 99 | +// Logs: "count: 3" (only once, not three times) |
| 100 | +``` |
| 101 | + |
| 102 | +--- |
| 103 | + |
| 104 | +### `computed(obj): T` |
| 105 | + |
| 106 | +Creates a proxy object with computed/derived properties. Each property is defined as a getter function that automatically re-runs when its dependencies change. |
21 | 107 |
|
22 | | -```js |
| 108 | +```ts |
23 | 109 | import { proxy } from 'valtio/vanilla'; |
24 | | -import { batch, computed, effect } from 'valtio-reactive'; |
| 110 | +import { computed } from 'valtio-reactive'; |
25 | 111 |
|
26 | 112 | const state = proxy({ count: 1 }); |
27 | 113 |
|
28 | 114 | const derived = computed({ |
29 | 115 | double: () => state.count * 2, |
| 116 | + quadruple: () => state.count * 4, |
30 | 117 | }); |
31 | 118 |
|
| 119 | +console.log(derived.double); // 2 |
| 120 | +console.log(derived.quadruple); // 4 |
| 121 | + |
| 122 | +state.count = 5; |
| 123 | + |
| 124 | +console.log(derived.double); // 10 |
| 125 | +console.log(derived.quadruple); // 20 |
| 126 | +``` |
| 127 | + |
| 128 | +The returned object is itself a `valtio` proxy, so you can use it with `effect`, `useSnapshot`, or any other `valtio` utility. |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## Usage with React |
| 133 | + |
| 134 | +While these primitives are framework-agnostic, they integrate seamlessly with `valtio`'s React bindings: |
| 135 | + |
| 136 | +```tsx |
| 137 | +import { proxy, useSnapshot } from 'valtio'; |
| 138 | +import { effect, computed } from 'valtio-reactive'; |
| 139 | + |
| 140 | +const state = proxy({ count: 0 }); |
| 141 | + |
| 142 | +// Computed values work with useSnapshot |
| 143 | +const derived = computed({ |
| 144 | + double: () => state.count * 2, |
| 145 | +}); |
| 146 | + |
| 147 | +// Side effects outside of React |
32 | 148 | effect(() => { |
33 | | - console.log('double count:', derived.double); |
| 149 | + console.log('Count changed:', state.count); |
34 | 150 | }); |
35 | 151 |
|
36 | | -setInterval(() => { |
37 | | - batch(() => { |
38 | | - state.count++; |
39 | | - state.count++; |
40 | | - }); |
41 | | -}, 1000); |
| 152 | +function Counter() { |
| 153 | + const snap = useSnapshot(state); |
| 154 | + const derivedSnap = useSnapshot(derived); |
| 155 | + |
| 156 | + return ( |
| 157 | + <div> |
| 158 | + <p>Count: {snap.count}</p> |
| 159 | + <p>Double: {derivedSnap.double}</p> |
| 160 | + <button onClick={() => state.count++}>+1</button> |
| 161 | + </div> |
| 162 | + ); |
| 163 | +} |
42 | 164 | ``` |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## TypeScript |
| 169 | + |
| 170 | +All exports are fully typed. The `computed` function infers types from your getter functions: |
| 171 | + |
| 172 | +```ts |
| 173 | +const state = proxy({ count: 1, name: 'test' }); |
| 174 | + |
| 175 | +const derived = computed({ |
| 176 | + double: () => state.count * 2, // inferred as number |
| 177 | + message: () => `Hello ${state.name}`, // inferred as string |
| 178 | +}); |
| 179 | + |
| 180 | +derived.double; // number |
| 181 | +derived.message; // string |
| 182 | +``` |
| 183 | + |
| 184 | +## License |
| 185 | + |
| 186 | +MIT |
0 commit comments