or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced.mdindex.mdlocal-state.mdobservers.mdstatic-rendering.md

local-state.mddocs/

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

```