Quick Start
This guide shows the essential patterns for using FobX with React.
1. Create a store
// stores/counter.ts
import { observable } from "@fobx/core"
export const counterStore = observable({
count: 0,
get doubled() {
return this.count * 2
},
increment() {
this.count++
},
decrement() {
this.count--
},
})
2. Wrap components with observer
// components/Counter.tsx
import { observer } from "@fobx/react"
import { counterStore } from "../stores/counter"
export const Counter = observer(() => (
<div>
<p>Count: {counterStore.count}</p>
<p>Doubled: {counterStore.doubled}</p>
<button onClick={() => counterStore.increment()}>+</button>
<button onClick={() => counterStore.decrement()}>-</button>
</div>
))
The component re-renders only when the specific observables it reads change.
3. Pass stores via props (optional)
For better testability, pass stores as props:
const TodoList = observer(({ store }: { store: TodoStore }) => (
<ul>
{store.visibleTodos.map((todo) => <li key={todo.id}>{todo.text}</li>)}
</ul>
))
4. Async data loading
Use generator methods for async operations — observable() automatically wraps
them as flows:
import { observable } from "@fobx/core"
class UserStore {
users: User[] = []
loading = false
constructor() {
observable(this)
}
*fetchUsers() {
this.loading = true
const res = yield fetch("/api/users")
this.users = yield res.json()
this.loading = false
}
}
export const userStore = new UserStore()
const UserList = observer(() => {
useEffect(() => {
userStore.fetchUsers()
}, [])
if (userStore.loading) return <p>Loading...</p>
return (
<ul>
{userStore.users.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
)
})
5. Local component state with useViewModel
For component-scoped reactive state:
import { observer, useViewModel, ViewModel } from "@fobx/react"
import { observable } from "@fobx/core"
class FormVM extends ViewModel<{ onSubmit: (value: string) => void }> {
text = ""
constructor(props: { onSubmit: (value: string) => void }) {
Calling `observable(this)` inside a `ViewModel` subclass only annotates the
subclass members. The base class keeps `props` computed, stores prop values by
reference, and leaves `update()` as a plain trackable method.
super(props)
observable(this)
}
get isValid() {
return this.text.length > 0
}
submit() {
if (this.isValid) this.props.onSubmit(this.text)
}
}
const Form = observer((props: { onSubmit: (value: string) => void }) => {
const vm = useViewModel(FormVM, props)
return (
<form
onSubmit={(e) => {
e.preventDefault()
vm.submit()
}}
>
<input value={vm.text} onChange={(e) => vm.text = e.target.value} />
<button disabled={!vm.isValid}>Submit</button>
</form>
)
})
Key rules
- Wrap with
observer— any component that reads observables must be wrapped, or it won’t re-render. - Don’t read early observable values before entering an
observeroruseObservertracked render. Destructuring inside the tracked render is fine; capturing values outside it is not. - Keep components small — smaller observer components track fewer dependencies and re-render less.