@watch-state/react
v2.1.1
Published
State manager of React
Maintainers
Readme
@watch-state/react
@watch-state/react provides React hooks for watch-state — a lightweight, high-performance reactive state engine.
Written in TypeScript and provides full type definitions out of the box.
Index
[ Install ]
[ Hooks ] useObservable • useSelector • useNewState • useNewCompute
[ Utils ] subscribe
[ Examples ] Aside Menu • Todo List • Async
[ Links ]
[ Issues ]
Install
🏠︎ / Install ↓
Requires React 18+ and watch-state 3.5+.
Use with any modern bundler (Vite, Webpack, Rollup, etc.) or framework (Next.js, Remix, etc.).
npm i @watch-state/reactWith watch-state
npm i watch-state @watch-state/reactHooks
🏠︎ / Hooks ↑ ↓
useObservable • useSelector • useNewState • useNewCompute
useObservable
🏠︎ / Hooks / useObservable ↓
Subscribe React components to watch-state changes with automatic re-render optimization.
Uses useSyncExternalStore for correct synchronization with React. Automatically subscribes via Watch and unsubscribes on unmount.
Watching Observables
Pass a State instance (or any Observable subclass, such as Compute) to useObservable() to subscribe to its changes. The hook returns the current value and triggers a re-render whenever the observable value changes.
import { State } from 'watch-state'
import { useObservable } from '@watch-state/react'
const $count = new State(0)
const increase = () => {
$count.value++
}
function Button () {
const count = useObservable($count)
return <button onClick={increase}>{count}</button>
}Batching Observables
This example demonstrates batching multiple state updates into one reactive event with createEvent. Clicking the button increments $a and $b atomically; the computed $sum then updates reactively without intermediate renders since all changes occur in a single update cycle.
import { State, Compute, createEvent } from 'watch-state'
import { useObservable } from '@watch-state/react'
const $a = new State(1)
const $b = new State(2)
const $sum = new Compute(() => $a.value + $b.value)
const increase = createEvent(() => {
$a.value++
$b.value++
})
function Button () {
const sum = useObservable($sum)
return <button onClick={increase}>{sum}</button>
}useSelector
🏠︎ / Hooks / useSelector ↑ ↓
You can pass a function to useSelector() to create a reactive selector that triggers re-renders only when the returned value changes (compared with Object.is).
The function may be called multiple times during a single render, so it must be pure and simple.
Uses useSyncExternalStore for correct synchronization with React.
Extracting Fields
This is ideal for lightweight, pure selections — e.g. extracting a field.
import { State } from 'watch-state'
import { useSelector } from '@watch-state/react'
const $user = new State({ name: 'Mike', age: 42 })
function UserName () {
const name = useSelector(() => $user.value.name)
return <div>Hello, {name}!</div>
}Combining Multiple Observables
You can combine multiple observables in one selector — the result updates reactively when any dependency changes.
import { State } from 'watch-state'
import { useSelector } from '@watch-state/react'
const $price = new State(100)
const $quantity = new State(2)
function Total () {
const total = useSelector(() => $price.value * $quantity.value)
return <div>Total: ${total}</div>
}Use Compute for complex computations or when returning new objects/arrays to avoid unnecessary recalculations or re-renders.
import { Compute, State } from 'watch-state'
import { useObservable } from '@watch-state/react'
const $products = new State(['apple', 'banana', 'cherry'])
const $list = new Compute(() => {
return $products.value.map(product => product.toUpperCase())
})
function Total () {
const list = useObservable($list)
return (
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
)
}Combining with React State or Props
You can combine useSelector() with React's useState or props to react to both watch-state changes and component state.
import { useState } from 'react'
import { State } from 'watch-state'
import { useSelector } from '@watch-state/react'
const $basePrice = new State(100)
function ProductCard ({ isMember }: { isMember: boolean }) {
const [quantity, setQuantity] = useState(1)
const total = useSelector(() => {
return $basePrice.value * quantity * (isMember ? 0.9 : 1)
})
return (
<div>
<p>Total: ${total}</p>
<button onClick={() => setQuantity(q => q + 1)}>+</button>
</div>
)
}Optimizing Expensive Computations with useMemo
For expensive computations or when returning new objects/arrays, use useMemo to avoid unnecessary recalculations.
import { State } from 'watch-state'
import { useObservable } from '@watch-state/react'
import { useMemo } from 'react'
const $items = new State(['apple', 'banana', 'cherry'])
function PrefixedItems ({ prefix }: { prefix: string }) {
const items = useObservable($items)
const prefixedItems = useMemo(() => {
return items.map(item => `${prefix} - ${item}`)
}, [items, prefix])
return <div>{prefixedItems.join(', ')}</div>
}useNewState
🏠︎ / Hooks / useNewState ↑ ↓
Create a State instance inside a React component that persists across re-renders and can be watched using useObservable.
import { Observable } from 'watch-state'
import { useObservable, useNewState } from '@watch-state/react'
function Parent () {
const $count = useNewState(0)
const handleClick = () => {
$count.value++
}
return (
<div>
<button onClick={handleClick}>+</button>
<Child $count={$count} />
</div>
)
}
function Child ({ $count }: { $count: Observable<number> }) {
const count = useObservable($count)
return <div>{count}</div>
}This example demonstrates a key optimization: when the button is clicked and $count.value changes, only the Child component re-renders (because it's subscribed to the observable), while the Parent component remains unchanged. This happens because useNewState creates a reactive state that doesn't trigger re-renders in the component where it's defined—only components that explicitly subscribe via useObservable or useSelector will re-render when the state changes.
Using Context for State Sharing
You can also use React Context to share reactive state across deeply nested components without prop drilling:
import { createContext, useContext } from 'react'
import { Observable } from 'watch-state'
import { useObservable, useNewState } from '@watch-state/react'
const CountContext = createContext<Observable<number> | undefined>(undefined)
const useCount = () => {
const $count = useContext(CountContext)
if (!$count) throw new Error('CountContext must be provided')
return useObservable($count)
}
function Parent () {
const $count = useNewState(0)
const handleClick = () => {
$count.value++
}
return (
<CountContext.Provider value={$count}>
<button onClick={handleClick}>+</button>
<Child />
</CountContext.Provider>
)
}
function Child () {
const count = useCount()
return <div>{count}</div>
}useNewCompute
🏠︎ / Hooks / useNewCompute ↑
Create a Compute instance inside a React component (persists across re-renders) that can be watched using useObservable.
import { useObservable, useNewCompute, useNewState } from '@watch-state/react'
function Parent () {
const $name = useNewState('Foo')
const $surname = useNewState('Bar')
const $fullName = useNewCompute(() => (
`${$name.value} ${$surname.value[0]}.`
))
const fullName = useObservable($fullName)
const handleClick = () => {
$surname.value = 'Baz'
}
return <button onClick={handleClick}>{fullName}</button>
}When the button is clicked, the component will not re-render even though $surname changed, because the computed value $fullName remains the same ("Foo B." before and after the change). This demonstrates the automatic optimization of useNewCompute - components only re-render when the computed value actually changes.
Using Props for Compute Sharing
You can pass a computed observable as a prop to child components. The Parent creates a Compute via useNewCompute and passes it down; only the Child re-renders when the computed value changes, while the Parent stays untouched.
import { Observable, State } from 'watch-state'
import { useObservable, useNewCompute } from '@watch-state/react'
const $name = new State('Mike')
const $surname = new State('Deight')
function Parent () {
const $fullName = useNewCompute(() => `${$name.value} ${$surname.value[0]}.`)
return <Child $fullName={$fullName} />
}
function Child ({ $fullName }: { $fullName: Observable<string> }) {
const fullName = useObservable($fullName)
return <div>{fullName}</div>
}Using dependency array for component state
Pass a dependency array as the second argument to useNewCompute to incorporate non-reactive values (props, React state) into the compute function. When any dependency in the array changes, the existing Compute instance triggers an update — recalculating its value without being recreated.
import { Observable, State } from 'watch-state'
import { useObservable, useNewCompute } from '@watch-state/react'
const $name = new State('Mike')
function Parent ({ surname }: { surname: string }) {
const $fullName = useNewCompute(() => (
`${$name.value} ${surname[0]}.`
), [surname])
return <Child $fullName={$fullName} />
}
function Child ({ $fullName }: { $fullName: Observable<string> }) {
const fullName = useObservable($fullName)
return <div>{fullName}</div>
}Using Context for Compute Sharing
You can use React Context to share a computed observable across deeply nested components without prop drilling:
import { createContext, useContext } from 'react'
import { Observable, State } from 'watch-state'
import { useObservable, useNewCompute } from '@watch-state/react'
const $name = new State('Mike')
const $surname = new State('Deight')
const FullNameContext = createContext<Observable<string> | undefined>(undefined)
const useFullName = () => {
const $fullName = useContext(FullNameContext)
if (!$fullName) throw new Error('FullNameContext must be provided')
return useObservable($fullName)
}
function Parent () {
const $fullName = useNewCompute(() => `${$name.value} ${$surname.value[0]}.`)
return (
<FullNameContext.Provider value={$fullName}>
<Child />
</FullNameContext.Provider>
)
}
function Child () {
const fullName = useFullName()
return <div>{fullName}</div>
}Utils
🏠︎ / Utils ↑ ↓
subscribe
🏠︎ / Utils / subscribe
Stable subscription factory for useSyncExternalStore with watch-state.
Used internally by useObservable and useSelector.
Creates a Watch instance that calls the provided callback on state changes.
import { useSyncExternalStore } from 'react'
import { subscribe } from '@watch-state/react'
import { State } from 'watch-state'
const $state = new State(0)
const value = useSyncExternalStore(subscribe, () => $state.value)
// Same as useObservable($state)Examples
🏠︎ / Examples ↑ ↓
Aside Menu • Todo List • Async
Aside Menu
🏠︎ / Examples / Aside Menu ↓
Two independent components sharing a single global State. The button toggles $show, while AsideMenu subscribes via useObservable and re-renders accordingly — the button itself never re-renders.
import { State } from 'watch-state'
import { useObservable } from '@watch-state/react'
const $show = new State(false)
function AsideMenuButton () {
const toggle = () => {
$show.value = !$show.value
}
return <button onClick={toggle} />
}
function AsideMenu () {
const show = useObservable($show)
return show ? <div>Aside Menu</div> : null
}Todo List
🏠︎ / Examples / Todo List ↑ ↓
A classic todo app showing how to combine a global reactive State with React's local useState. The todo list is shared via useObservable, while the input field stays in component-local state.
import { useState } from 'react'
import { State } from 'watch-state'
import { useObservable } from '@watch-state/react'
interface Todo {
id: number
text: string
done: boolean
}
const $todos = new State<Todo[]>([])
let nextId = 1
const addTodo = (text: string) => {
$todos.value.push({ id: nextId++, text, done: false })
$todos.update()
}
const toggleTodo = (todoId: number) => {
$todos.value = $todos.value.map(todo =>
todoId === todo.id ? { ...todo, done: !todo.done } : todo
)
}
function TodoList () {
const todos = useObservable($todos)
const [text, setText] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (text.trim()) {
addTodo(text.trim())
setText('')
}
}
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={text}
onChange={e => setText(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(({ id, done, text }) => (
<li
key={id}
onClick={() => toggleTodo(id)}
style={{ textDecoration: done ? 'line-through' : 'none' }}
>
{text}
</li>
))}
</ul>
</div>
)
}Async
🏠︎ / Examples / Async ↑
This example demonstrates integration with @watch-state/async for reactive data fetching. Async is an observable that wraps a Promise-returning function. Use useObservable to subscribe to the resolved value and useSelector to reactively track its loading, loaded, and error properties.
import { useObservable, useSelector } from '@watch-state/react'
import Async from '@watch-state/async'
const $api = new Async(
() => fetch('/api/test')
.then(r => r.json())
)
function User () {
const value = useObservable($api)
const loading = useSelector(() => $api.loading)
const loaded = useSelector(() => $api.loaded)
const error = useSelector(() => $api.error)
if (error) {
return <div>Error!</div>
}
if (!loaded) {
return <div>Loading</div>
}
return (
<div className={loading && 'loading'}>
{value.some.fields}
</div>
)
}Links
🏠︎ / Links ↑ ↓
Issues
🏠︎ / Issues ↑
If you find a bug or have a suggestion, please file an issue on GitHub
