0
# Local Observable State
1
2
Hooks for creating and managing observable state within React components. These hooks provide component-local observable state that integrates seamlessly with the React component lifecycle and MobX reactivity system.
3
4
## Capabilities
5
6
### useLocalObservable Hook
7
8
Creates an observable object using the provided initializer function. This is the recommended way to create local observable state in functional components.
9
10
```typescript { .api }
11
/**
12
* Creates an observable object with the given properties, methods and computed values
13
* @param initializer - Function that returns the initial state object
14
* @param annotations - Optional MobX annotations for fine-tuned observability control
15
* @returns Observable state object with stable reference
16
*/
17
function useLocalObservable<TStore extends Record<string, any>>(
18
initializer: () => TStore,
19
annotations?: AnnotationsMap<TStore, never>
20
): TStore;
21
```
22
23
**Usage Examples:**
24
25
```typescript
26
import { observer, useLocalObservable } from "mobx-react-lite";
27
import { computed, action } from "mobx";
28
29
// Basic observable state
30
const Counter = observer(() => {
31
const store = useLocalObservable(() => ({
32
count: 0,
33
increment() {
34
this.count++;
35
},
36
decrement() {
37
this.count--;
38
},
39
get doubled() {
40
return this.count * 2;
41
}
42
}));
43
44
return (
45
<div>
46
<p>Count: {store.count}</p>
47
<p>Doubled: {store.doubled}</p>
48
<button onClick={store.increment}>+</button>
49
<button onClick={store.decrement}>-</button>
50
</div>
51
);
52
});
53
54
// With explicit annotations
55
const TodoList = observer(() => {
56
const store = useLocalObservable(
57
() => ({
58
todos: [],
59
filter: 'all',
60
addTodo(text: string) {
61
this.todos.push({ id: Date.now(), text, completed: false });
62
},
63
toggleTodo(id: number) {
64
const todo = this.todos.find(t => t.id === id);
65
if (todo) todo.completed = !todo.completed;
66
},
67
get visibleTodos() {
68
switch (this.filter) {
69
case 'active': return this.todos.filter(t => !t.completed);
70
case 'completed': return this.todos.filter(t => t.completed);
71
default: return this.todos;
72
}
73
}
74
}),
75
{
76
addTodo: action,
77
toggleTodo: action,
78
visibleTodos: computed
79
}
80
);
81
82
return (
83
<div>
84
<input
85
onKeyDown={(e) => {
86
if (e.key === 'Enter') {
87
store.addTodo(e.currentTarget.value);
88
e.currentTarget.value = '';
89
}
90
}}
91
/>
92
{store.visibleTodos.map(todo => (
93
<div key={todo.id} onClick={() => store.toggleTodo(todo.id)}>
94
{todo.completed ? '✓' : '○'} {todo.text}
95
</div>
96
))}
97
</div>
98
);
99
});
100
101
// Integration with props
102
import { useEffect } from "react";
103
104
interface User {
105
name: string;
106
email: string;
107
}
108
109
// Mock API function
110
declare function fetchUser(userId: string): Promise<User>;
111
112
const UserProfile = observer(({ userId }: { userId: string }) => {
113
const store = useLocalObservable(() => ({
114
user: null as User | null,
115
loading: true,
116
error: null as string | null,
117
118
async loadUser() {
119
this.loading = true;
120
this.error = null;
121
try {
122
this.user = await fetchUser(userId);
123
} catch (err) {
124
this.error = err.message;
125
} finally {
126
this.loading = false;
127
}
128
}
129
}));
130
131
useEffect(() => {
132
store.loadUser();
133
}, [userId, store]);
134
135
if (store.loading) return <div>Loading...</div>;
136
if (store.error) return <div>Error: {store.error}</div>;
137
if (!store.user) return <div>User not found</div>;
138
139
return (
140
<div>
141
<h1>{store.user.name}</h1>
142
<p>{store.user.email}</p>
143
</div>
144
);
145
});
146
```
147
148
### useLocalStore Hook (Deprecated)
149
150
Creates an observable store using an initializer function. This hook is deprecated in favor of `useLocalObservable`.
151
152
```typescript { .api }
153
/**
154
* @deprecated Use useLocalObservable instead
155
* Creates local observable state with optional source synchronization
156
* @param initializer - Function that returns the initial store object
157
* @param current - Optional source object to synchronize with
158
* @returns Observable store object
159
*/
160
function useLocalStore<TStore extends Record<string, any>>(
161
initializer: () => TStore
162
): TStore;
163
164
function useLocalStore<TStore extends Record<string, any>, TSource extends object>(
165
initializer: (source: TSource) => TStore,
166
current: TSource
167
): TStore;
168
```
169
170
**Migration Example:**
171
172
```typescript
173
// Old approach (deprecated)
174
const store = useLocalStore(() => ({
175
count: 0,
176
increment() { this.count++; }
177
}));
178
179
// New approach (recommended)
180
const store = useLocalObservable(() => ({
181
count: 0,
182
increment() { this.count++; }
183
}));
184
```
185
186
### useAsObservableSource Hook (Deprecated)
187
188
Converts any set of values into an observable object with stable reference. This hook is deprecated due to anti-pattern of updating observables during rendering.
189
190
```typescript { .api }
191
/**
192
* @deprecated Use useEffect to synchronize non-observable values instead
193
* Converts values into an observable object with stable reference
194
* @param current - Source object to make observable
195
* @returns Observable version of the source object
196
*/
197
function useAsObservableSource<TSource extends object>(current: TSource): TSource;
198
```
199
200
**Migration Example:**
201
202
```typescript
203
// Old approach (deprecated)
204
function Measurement({ unit }: { unit: string }) {
205
const observableProps = useAsObservableSource({ unit });
206
const state = useLocalStore(() => ({
207
length: 0,
208
get lengthWithUnit() {
209
return observableProps.unit === "inch"
210
? `${this.length / 2.54} inch`
211
: `${this.length} cm`;
212
}
213
}));
214
215
return <h1>{state.lengthWithUnit}</h1>;
216
}
217
218
// New approach (recommended)
219
import { useEffect } from "react";
220
221
function Measurement({ unit }: { unit: string }) {
222
const state = useLocalObservable(() => ({
223
length: 0,
224
unit: unit,
225
get lengthWithUnit() {
226
return this.unit === "inch"
227
? `${this.length / 2.54} inch`
228
: `${this.length} cm`;
229
}
230
}));
231
232
// Sync prop changes to observable state
233
useEffect(() => {
234
state.unit = unit;
235
}, [unit, state]);
236
237
return <h1>{state.lengthWithUnit}</h1>;
238
}
239
```
240
241
## Important Notes
242
243
### Best Practices
244
245
- **Use initializer functions**: Always pass a function to create fresh state for each component instance
246
- **Avoid closures**: Don't capture variables from component scope in the initializer to prevent stale closures
247
- **Stable references**: The returned observable object has a stable reference throughout the component lifecycle
248
- **Auto-binding**: Methods are automatically bound to the observable object (`autoBind: true`)
249
- **Computed values**: Use getter functions for derived state that should be cached and reactive
250
251
### Integration with React
252
253
- **Component lifecycle**: Observable state is created once per component instance and disposed on unmount
254
- **Effect synchronization**: Use `useEffect` to synchronize props or external state with observable state
255
- **Error boundaries**: Exceptions in observable methods are properly caught by React error boundaries
256
- **Development tools**: Observable state is debuggable through MobX developer tools
257
258
### Performance Considerations
259
260
- **Lazy evaluation**: Computed values are only recalculated when their dependencies change
261
- **Automatic batching**: Multiple state changes in the same tick are batched for optimal rendering
262
- **Memory management**: Reactions and computed values are automatically disposed when component unmounts
263
- **Shallow observation**: Only the direct properties of the returned object are observable by default
264
265
## Types
266
267
```typescript { .api }
268
// MobX annotations from mobx package
269
type Annotation = {
270
annotationType_: string;
271
make_(adm: any, key: PropertyKey, descriptor: PropertyDescriptor, source: object): any;
272
extend_(adm: any, key: PropertyKey, descriptor: PropertyDescriptor, proxyTrap: boolean): boolean | null;
273
options_?: any;
274
};
275
276
type AnnotationMapEntry = Annotation | true | false;
277
278
type AnnotationsMap<T, AdditionalKeys extends PropertyKey> = {
279
[K in keyof T | AdditionalKeys]?: AnnotationMapEntry;
280
};
281
```