UNPKG

24.3 kBMarkdownView Raw
1# mobx-react
2
3[![CircleCI](https://circleci.com/gh/mobxjs/mobx-react.svg?style=svg)](https://circleci.com/gh/mobxjs/mobx-react)
4[![CDNJS](https://img.shields.io/cdnjs/v/mobx-react.svg)](https://cdnjs.com/libraries/mobx-react)
5[![Minzipped size](https://img.shields.io/bundlephobia/minzip/mobx-react.svg)](https://bundlephobia.com/result?p=mobx-react)
6[![Discuss on Github](https://img.shields.io/badge/discuss%20on-GitHub-orange)](https://github.com/mobxjs/mobx/discussions)
7[![View changelog](https://img.shields.io/badge/changelogs.xyz-Explore%20Changelog-brightgreen)](https://changelogs.xyz/mobx-react)
8
9Package with React component wrapper for combining React with MobX.
10Exports the `observer` decorator and other utilities.
11For documentation, see the [MobX](https://mobx.js.org) project.
12This package supports both React and React Native.
13
14## Compatibility matrix
15
16Only the latest version is actively maintained. If you're missing a fix or a feature in older version, consider upgrading or using [patch-package](https://www.npmjs.com/package/patch-package)
17
18| NPM Version | Support MobX version | Supported React versions | Added support for: |
19| ----------- | -------------------- | ------------------------ | -------------------------------------------------------------------------------- |
20| v9 | 6.\* | >16.8 | Hooks, React 18.2 in strict mode |
21| v7 | 6.\* | >16.8 < 18.2 | Hooks |
22| v6 | 4.\* / 5.\* | >16.8 <18 | Hooks |
23| v5 | 4.\* / 5.\* | >0.13 <18 | No, but it is possible to use `<Observer>` sections inside hook based components |
24
25mobx-react 6 / 7 is a repackage of the smaller [mobx-react-lite](https://github.com/mobxjs/mobx/tree/main/packages/mobx-react-lite) package + following features from the `mobx-react@5` package added:
26
27- Support for class based components for `observer` and `@observer`
28- `Provider / inject` to pass stores around (but consider to use `React.createContext` instead)
29- `PropTypes` to describe observable based property checkers (but consider to use TypeScript instead)
30- The `disposeOnUnmount` utility / decorator to easily clean up resources such as reactions created in your class based components.
31
32## Installation
33
34`npm install mobx-react --save`
35
36Or CDN: https://unpkg.com/mobx-react (UMD namespace: `mobxReact`)
37
38```javascript
39import { observer } from "mobx-react"
40```
41
42This package provides the bindings for MobX and React.
43See the [official documentation](https://mobx.js.org/react-integration.html) for how to get started.
44
45For greenfield projects you might want to consider to use [mobx-react-lite](https://github.com/mobxjs/mobx/tree/main/packages/mobx-react-lite), if you intend to only use function based components. `React.createContext` can be used to pass stores around.
46
47## API documentation
48
49Please check [mobx.js.org](https://mobx.js.org/) for the general documentation. The documentation below highlights some specifics.
50
51### `observer(component)`
52
53Function (and decorator) that converts a React component definition, React component class, or stand-alone render function, into a reactive component. A converted component will track which observables are used by its effective `render` and automatically re-render the component when one of these values changes.
54
55#### Functional Components
56
57`React.memo` is automatically applied to functional components provided to `observer`. `observer` does not accept a functional component already wrapped in `React.memo`, or an `observer`, in order to avoid consequences that might arise as a result of wrapping it twice.
58
59#### Class Components
60
61`shouldComponentUpdate` is not supported. As such, it is recommended that class components extend `React.PureComponent`. The `observer` will automatically patch non-pure class components with an internal implementation of `React.PureComponent` if necessary.
62
63Extending `observer` class components is not supported. Always apply `observer` only on the last class in the inheritance chain.
64
65See the [MobX](https://mobx.js.org/react-integration.html#react-integration) documentation for more details.
66
67```javascript
68import { observer } from "mobx-react"
69
70// ---- ES6 syntax ----
71const TodoView = observer(
72 class TodoView extends React.Component {
73 render() {
74 return <div>{this.props.todo.title}</div>
75 }
76 }
77)
78
79// ---- ESNext syntax with decorator syntax enabled ----
80@observer
81class TodoView extends React.Component {
82 render() {
83 return <div>{this.props.todo.title}</div>
84 }
85}
86
87// ---- or just use function components: ----
88const TodoView = observer(({ todo }) => <div>{todo.title}</div>)
89```
90
91##### Note on using props and state in derivations
92
93`mobx-react` version 6 and lower would automatically turn `this.state` and `this.props` into observables.
94This has the benefit that computed properties and reactions were able to observe those.
95However, since this pattern is fundamentally incompatible with `StrictMode` in React 18.2 and higher, this behavior has been removed in React 18.
96
97As a result, we recommend to no longer mark properties as `@computed` in observer components if they depend on `this.state` or `this.props`.
98
99```javascript
100@observer
101class Doubler extends React.Component<{ counter: number }> {
102 @computed // BROKEN! <-- @computed should be removed in mobx-react > 7
103 get doubleValue() {
104 // Changes to this.props will no longer be detected properly, to fix it,
105 // remove the @computed annotation.
106 return this.props * 2
107 }
108
109 render() {
110 return <div>{this.doubleValue}</div>
111 }
112}
113```
114
115Similarly, reactions will no longer respond to `this.state` / `this.props`. This can be overcome by creating an observable copy:
116
117```javascript
118@observer
119class Alerter extends React.Component<{ counter: number }> {
120 @observable observableCounter: number
121 reactionDisposer
122
123 constructor(props) {
124 this.observableCounter = counter
125 }
126
127 componentDidMount() {
128 // set up a reaction, by observing the observable,
129 // rather than the prop which is non-reactive:
130 this.reactionDisposer = autorun(() => {
131 if (this.observableCounter > 10) {
132 alert("Reached 10!")
133 }
134 })
135 }
136
137 componentDidUpdate() {
138 // sync the observable from props
139 this.observableCounter = this.props.counter
140 }
141
142 componentWillUnmount() {
143 this.reactionDisposer()
144 }
145
146 render() {
147 return <div>{this.props.counter}</div>
148 }
149}
150```
151
152MobX-react will try to detect cases where `this.props`, `this.state` or `this.context` are used by any other derivation than the `render` method of the owning component and throw.
153This is to make sure that neither computed properties, nor reactions, nor other components accidentally rely on those fields to be reactive.
154
155This includes cases where a render callback is passed to a child, that will read from the props or state of a parent component.
156As a result, passing a function that might later read a property of a parent in a reactive context will throw as well.
157Instead, when using a callback function that is being passed to an `observer` based child, the capture should be captured locally first:
158
159```javascript
160@observer
161class ChildWrapper extends React.Component<{ counter: number }> {
162 render() {
163 // Collapsible is an observer component that should respond to this.counter,
164 // if it is expanded
165
166 // BAD:
167 return <Collapsible onRenderContent={() => <h1>{this.props.counter}</h1>} />
168
169 // GOOD: (causes to pass down a fresh callback whenever counter changes,
170 // that doesn't depend on its parents props)
171 const counter = this.props.counter
172 return <Collapsible onRenderContent={() => <h1>{counter}</h1>} />
173 }
174}
175```
176
177### `Observer`
178
179`Observer` is a React component, which applies `observer` to an anonymous region in your component.
180It takes as children a single, argumentless function which should return exactly one React component.
181The rendering in the function will be tracked and automatically re-rendered when needed.
182This can come in handy when needing to pass render function to external components (for example the React Native listview), or if you
183dislike the `observer` decorator / function.
184
185```javascript
186class App extends React.Component {
187 render() {
188 return (
189 <div>
190 {this.props.person.name}
191 <Observer>{() => <div>{this.props.person.name}</div>}</Observer>
192 </div>
193 )
194 }
195}
196
197const person = observable({ name: "John" })
198
199ReactDOM.render(<App person={person} />, document.body)
200person.name = "Mike" // will cause the Observer region to re-render
201```
202
203In case you are a fan of render props, you can use that instead of children. Be advised, that you cannot use both approaches at once, children have a precedence.
204Example
205
206```javascript
207class App extends React.Component {
208 render() {
209 return (
210 <div>
211 {this.props.person.name}
212 <Observer render={() => <div>{this.props.person.name}</div>} />
213 </div>
214 )
215 }
216}
217
218const person = observable({ name: "John" })
219
220ReactDOM.render(<App person={person} />, document.body)
221person.name = "Mike" // will cause the Observer region to re-render
222```
223
224### `useLocalObservable` hook
225
226Local observable state can be introduced by using the `useLocalObservable` hook, that runs once to create an observable store. A quick example would be:
227
228```javascript
229import { useLocalObservable, Observer } from "mobx-react-lite"
230
231const Todo = () => {
232 const todo = useLocalObservable(() => ({
233 title: "Test",
234 done: true,
235 toggle() {
236 this.done = !this.done
237 }
238 }))
239
240 return (
241 <Observer>
242 {() => (
243 <h1 onClick={todo.toggle}>
244 {todo.title} {todo.done ? "[DONE]" : "[TODO]"}
245 </h1>
246 )}
247 </Observer>
248 )
249}
250```
251
252When using `useLocalObservable`, all properties of the returned object will be made observable automatically, getters will be turned into computed properties, and methods will be bound to the store and apply mobx transactions automatically. If new class instances are returned from the initializer, they will be kept as is.
253
254It is important to realize that the store is created only once! It is not possible to specify dependencies to force re-creation, _nor should you directly be referring to props for the initializer function_, as changes in those won't propagate.
255
256Instead, if your store needs to refer to props (or `useState` based local state), the `useLocalObservable` should be combined with the `useAsObservableSource` hook, see below.
257
258Note that in many cases it is possible to extract the initializer function to a function outside the component definition. Which makes it possible to test the store itself in a more straight-forward manner, and avoids creating the initializer closure on each re-render.
259
260_Note: using `useLocalObservable` is mostly beneficial for really complex local state, or to obtain more uniform code base. Note that using a local store might conflict with future React features like concurrent rendering._
261
262### Server Side Rendering with `enableStaticRendering`
263
264When using server side rendering, normal lifecycle hooks of React components are not fired, as the components are rendered only once.
265Since components are never unmounted, `observer` components would in this case leak memory when being rendered server side.
266To avoid leaking memory, call `enableStaticRendering(true)` when using server side rendering.
267
268```javascript
269import { enableStaticRendering } from "mobx-react"
270
271enableStaticRendering(true)
272```
273
274This makes sure the component won't try to react to any future data changes.
275
276### Which components should be marked with `observer`?
277
278The simple rule of thumb is: _all components that render observable data_.
279If you don't want to mark a component as observer, for example to reduce the dependencies of a generic component package, make sure you only pass it plain data.
280
281### Enabling decorators (optional)
282
283Decorators are currently a stage-2 ESNext feature. How to enable them is documented [here](https://mobx.js.org/enabling-decorators.html#enabling-decorators-).
284
285### Should I still use smart and dumb components?
286
287See this [thread](https://www.reddit.com/r/reactjs/comments/4vnxg5/free_eggheadio_course_learn_mobx_react_in_30/d61oh0l).
288TL;DR: the conceptual distinction makes a lot of sense when using MobX as well, but use `observer` on all components.
289
290### `PropTypes`
291
292MobX-react provides the following additional `PropTypes` which can be used to validate against MobX structures:
293
294- `observableArray`
295- `observableArrayOf(React.PropTypes.number)`
296- `observableMap`
297- `observableObject`
298- `arrayOrObservableArray`
299- `arrayOrObservableArrayOf(React.PropTypes.number)`
300- `objectOrObservableObject`
301
302Use `import { PropTypes } from "mobx-react"` to import them, then use for example `PropTypes.observableArray`
303
304### `Provider` and `inject`
305
306_Note: usually there is no need anymore to use `Provider` / `inject` in new code bases; most of its features are now covered by `React.createContext`._
307
308`Provider` is a component that can pass stores (or other stuff) using React's context mechanism to child components.
309This is useful if you have things that you don't want to pass through multiple layers of components explicitly.
310
311`inject` can be used to pick up those stores. It is a higher order component that takes a list of strings and makes those stores available to the wrapped component.
312
313Example (based on the official [context docs](https://facebook.github.io/react/docs/context.html#passing-info-automatically-through-a-tree)):
314
315```javascript
316@inject("color")
317@observer
318class Button extends React.Component {
319 render() {
320 return <button style={{ background: this.props.color }}>{this.props.children}</button>
321 }
322}
323
324class Message extends React.Component {
325 render() {
326 return (
327 <div>
328 {this.props.text} <Button>Delete</Button>
329 </div>
330 )
331 }
332}
333
334class MessageList extends React.Component {
335 render() {
336 const children = this.props.messages.map(message => <Message text={message.text} />)
337 return (
338 <Provider color="red">
339 <div>{children}</div>
340 </Provider>
341 )
342 }
343}
344```
345
346Notes:
347
348- It is possible to read the stores provided by `Provider` using `React.useContext`, by using the `MobXProviderContext` context that can be imported from `mobx-react`.
349- If a component asks for a store and receives a store via a property with the same name, the property takes precedence. Use this to your advantage when testing!
350- When using both `@inject` and `@observer`, make sure to apply them in the correct order: `observer` should be the inner decorator, `inject` the outer. There might be additional decorators in between.
351- The original component wrapped by `inject` is available as the `wrappedComponent` property of the created higher order component.
352
353#### "The set of provided stores has changed" error
354
355Values provided through `Provider` should be final. Make sure that if you put things in `context` that might change over time, that they are `@observable` or provide some other means to listen to changes, like callbacks. However, if your stores will change over time, like an observable value of another store, MobX will throw an error.
356This restriction exists mainly for legacy reasons. If you have a scenario where you need to modify the set of stores, please leave a comment about it in this issue https://github.com/mobxjs/mobx-react/issues/745. Or a preferred way is to [use React Context](https://reactjs.org/docs/context.html) directly which does not have this restriction.
357
358#### Inject as function
359
360The above example in ES5 would start like:
361
362```javascript
363var Button = inject("color")(
364 observer(
365 class Button extends Component {
366 /* ... etc ... */
367 }
368 )
369)
370```
371
372A functional stateless component would look like:
373
374```javascript
375var Button = inject("color")(
376 observer(({ color }) => {
377 /* ... etc ... */
378 })
379)
380```
381
382#### Customizing inject
383
384Instead of passing a list of store names, it is also possible to create a custom mapper function and pass it to inject.
385The mapper function receives all stores as argument, the properties with which the components are invoked and the context, and should produce a new set of properties,
386that are mapped into the original:
387
388`mapperFunction: (allStores, props, context) => additionalProps`
389
390Since version 4.0 the `mapperFunction` itself is tracked as well, so it is possible to do things like:
391
392```javascript
393const NameDisplayer = ({ name }) => <h1>{name}</h1>
394
395const UserNameDisplayer = inject(stores => ({
396 name: stores.userStore.name
397}))(NameDisplayer)
398
399const user = mobx.observable({
400 name: "Noa"
401})
402
403const App = () => (
404 <Provider userStore={user}>
405 <UserNameDisplayer />
406 </Provider>
407)
408
409ReactDOM.render(<App />, document.body)
410```
411
412_N.B. note that in this *specific* case neither `NameDisplayer` nor `UserNameDisplayer` needs to be decorated with `observer`, since the observable dereferencing is done in the mapper function_
413
414#### Using `PropTypes` and `defaultProps` and other static properties in combination with `inject`
415
416Inject wraps a new component around the component you pass into it.
417This means that assigning a static property to the resulting component, will be applied to the HoC, and not to the original component.
418So if you take the following example:
419
420```javascript
421const UserName = inject("userStore")(({ userStore, bold }) => someRendering())
422
423UserName.propTypes = {
424 bold: PropTypes.boolean.isRequired,
425 userStore: PropTypes.object.isRequired // will always fail
426}
427```
428
429The above propTypes are incorrect, `bold` needs to be provided by the caller of the `UserName` component and is checked by React.
430However, `userStore` does not need to be required! Although it is required for the original stateless function component, it is not
431required for the resulting inject component. After all, the whole point of that component is to provide that `userStore` itself.
432
433So if you want to make assertions on the data that is being injected (either stores or data resulting from a mapper function), the propTypes
434should be defined on the _wrapped_ component. Which is available through the static property `wrappedComponent` on the inject component:
435
436```javascript
437const UserName = inject("userStore")(({ userStore, bold }) => someRendering())
438
439UserName.propTypes = {
440 bold: PropTypes.boolean.isRequired // could be defined either here ...
441}
442
443UserName.wrappedComponent.propTypes = {
444 // ... or here
445 userStore: PropTypes.object.isRequired // correct
446}
447```
448
449The same principle applies to `defaultProps` and other static React properties.
450Note that it is not allowed to redefine `contextTypes` on `inject` components (but is possible to define it on `wrappedComponent`)
451
452Finally, mobx-react will automatically move non React related static properties from wrappedComponent to the inject component so that all static fields are
453actually available to the outside world without needing `.wrappedComponent`.
454
455#### Strongly typing inject
456
457##### With TypeScript
458
459`inject` also accepts a function (`(allStores, nextProps, nextContext) => additionalProps`) that can be used to pick all the desired stores from the available stores like this.
460The `additionalProps` will be merged into the original `nextProps` before being provided to the next component.
461
462```typescript
463import { IUserStore } from "myStore"
464
465@inject(allStores => ({
466 userStore: allStores.userStore as IUserStore
467}))
468class MyComponent extends React.Component<{ userStore?: IUserStore; otherProp: number }, {}> {
469 /* etc */
470}
471```
472
473Make sure to mark `userStore` as an optional property. It should not (necessarily) be passed in by parent components at all!
474
475Note: If you have strict null checking enabled, you could muffle the nullable type by using the `!` operator:
476
477```
478public render() {
479 const {a, b} = this.store!
480 // ...
481}
482```
483
484#### Testing store injection
485
486It is allowed to pass any declared store in directly as a property as well. This makes it easy to set up individual component tests without a provider.
487
488So if you have in your app something like:
489
490```javascript
491<Provider profile={profile}>
492 <Person age={"30"} />
493</Provider>
494```
495
496In your test you can easily test the `Person` component by passing the necessary store as prop directly:
497
498```
499const profile = new Profile()
500const mountedComponent = mount(
501 <Person age={'30'} profile={profile} />
502)
503```
504
505Bear in mind that using shallow rendering won't provide any useful results when testing injected components; only the injector will be rendered.
506To test with shallow rendering, instantiate the `wrappedComponent` instead: `shallow(<Person.wrappedComponent />)`
507
508### disposeOnUnmount(componentInstance, propertyKey | function | function[])
509
510Function (and decorator) that makes sure a function (usually a disposer such as the ones returned by `reaction`, `autorun`, etc.) is automatically executed as part of the componentWillUnmount lifecycle event.
511
512```javascript
513import { disposeOnUnmount } from "mobx-react"
514
515class SomeComponent extends React.Component {
516 // decorator version
517 @disposeOnUnmount
518 someReactionDisposer = reaction(...)
519
520 // decorator version with arrays
521 @disposeOnUnmount
522 someReactionDisposers = [
523 reaction(...),
524 reaction(...)
525 ]
526
527
528 // function version over properties
529 someReactionDisposer = disposeOnUnmount(this, reaction(...))
530
531 // function version inside methods
532 componentDidMount() {
533 // single function
534 disposeOnUnmount(this, reaction(...))
535
536 // or function array
537 disposeOnUnmount(this, [
538 reaction(...),
539 reaction(...)
540 ])
541 }
542}
543```
544
545## DevTools
546
547`mobx-react@6` and higher are no longer compatible with the mobx-react-devtools.
548That is, the MobX react devtools will no longer show render timings or dependency trees of the component.
549The reason is that the standard React devtools are also capable of highlighting re-rendering components.
550And the dependency tree of a component can now be inspected by the standard devtools as well, as shown in the image below:
551
552![hooks.png](hooks.png)
553
554## FAQ
555
556**Should I use `observer` for each component?**
557
558You should use `observer` on every component that displays observable data.
559Even the small ones. `observer` allows components to render independently from their parent and in general this means that
560the more you use `observer`, the better the performance become.
561The overhead of `observer` itself is negligible.
562See also [Do child components need `@observer`?](https://github.com/mobxjs/mobx/issues/101)
563
564**I see React warnings about `forceUpdate` / `setState` from React**
565
566The following warning will appear if you trigger a re-rendering between instantiating and rendering a component:
567
568```
569
570Warning: forceUpdate(...): Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.`
571
572```
573
574-- or --
575
576```
577
578Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
579
580```
581
582Usually this means that (another) component is trying to modify observables used by this components in their `constructor` or `getInitialState` methods.
583This violates the React Lifecycle, `componentWillMount` should be used instead if state needs to be modified before mounting.