Skip to content
This repository has been archived by the owner on Dec 31, 2020. It is now read-only.

WIP: V4 #154

Merged
merged 51 commits into from
Nov 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d81d33a
attempt to made this.props observable, see #124
mweststrate Oct 6, 2016
0bcf70b
made `this.props` observable!
mweststrate Oct 6, 2016
482ae47
well if props / state is observable... why SCU at all?
mweststrate Oct 6, 2016
753046a
Dispose reaction on error
Oct 14, 2016
a974277
Added a failing test case when function passed to props.
Strate Oct 17, 2016
fa427ad
Add `asReference` modifier for each property of component
Strate Oct 17, 2016
8c678c2
Make `this.props` setter an action
Strate Oct 17, 2016
4e3dc99
Use same techique to make props & state observable
Strate Oct 17, 2016
6690f91
Add failing test case with broken lifecycle methods
Strate Oct 18, 2016
eca5341
Tried to implement smart observable of props & state of component.
Strate Oct 18, 2016
d576192
Remove missed console.log
Strate Oct 18, 2016
1e83e1a
Debounce reportChanged
Strate Oct 18, 2016
f5be8f4
Skip render while props/state change `reportChanged()` call.
Strate Oct 18, 2016
888dc72
Remove lodash.debounce as it is not needed anymore
Strate Oct 18, 2016
0310ba1
Remove unused import of `lodash.debounce`.
Strate Oct 21, 2016
fefed40
Merge branch 'master' into observable-props-124
mweststrate Nov 7, 2016
861d46d
additional test
mweststrate Nov 7, 2016
260746d
Merge branch 'observable-props-124' into v4
mweststrate Nov 7, 2016
124acc3
Merge branch 'master' into v4
mweststrate Nov 7, 2016
48319cc
Added test for #134
Nov 7, 2016
059f8ea
Merge branch 'master' into tryForceUpdate
Nov 7, 2016
c67781c
Added test for #134
Nov 8, 2016
479c370
Merge branch 'tryForceUpdate' of https://github.com/andykog/mobx-reac…
mweststrate Nov 8, 2016
7335a31
Added minified build, fixes #147
mweststrate Nov 8, 2016
981b601
observer: check setState always trigger render
Strate Nov 8, 2016
cd154c2
Implemented `Observer` regions, see #138
mweststrate Nov 9, 2016
eee7e1b
added strict mode, to avoid test issues
mweststrate Nov 9, 2016
1c08739
Implemented #111; track mapper function in
mweststrate Nov 9, 2016
7243a9d
removed some duplicate tests
mweststrate Nov 9, 2016
666577b
Proposal to forward propTypes and defaultProps, see #120, #142
mweststrate Nov 9, 2016
36198d9
Generate better component names for inject
mweststrate Nov 9, 2016
7f4d5fd
Improved docs, removed warning on PropTypes
mweststrate Nov 10, 2016
e7daa54
improved isInjector check for inject with mapper
mweststrate Nov 11, 2016
8faa01b
Using `observer` to inject stores is now deprecated
mweststrate Nov 11, 2016
77b6751
Merge branch 'improve-proptypes-on-inject' into v4
mweststrate Nov 11, 2016
e104e7c
added test for deprecation check
mweststrate Nov 11, 2016
a41dd32
Improved changelog
mweststrate Nov 11, 2016
e7055a8
Fixed typos in changelog
mweststrate Nov 11, 2016
db86c8a
add support of es6 and refactor tests
A-gambit Nov 11, 2016
15fdb01
add react support to test and refactor them
A-gambit Nov 12, 2016
4cda30b
fix travis
A-gambit Nov 12, 2016
96bc4ab
change version of tape-run
A-gambit Nov 12, 2016
927b575
Merge pull request #158 from mobxjs/es6-unit-tests
A-gambit Nov 14, 2016
d201972
more babel, less DRY!
mweststrate Nov 14, 2016
ef75a40
fixed endless forceUpdateLoop
mweststrate Nov 14, 2016
8dc833e
added test showing that inject could be more optimal, see #111
mweststrate Nov 14, 2016
d58ee61
Made observer pure by default
mweststrate Nov 15, 2016
883e387
Merge branch '160-pure-observer' into v4
mweststrate Nov 17, 2016
23ac8cf
mobx-react now uses React's batched updates to more optimally rerende…
mweststrate Nov 17, 2016
83e7e93
RC 3
mweststrate Nov 17, 2016
67d42bb
Fixed packaging issue, see #162
mweststrate Nov 20, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ custom.js
custom.d.ts
index.js
index.d.ts
index.min.js
.vscode
.idea
test/browser/test_bundle.js
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
language: node_js
script: npm run travis
script: npm run test:travis
after_success:
- cat ./coverage/lcov.info|./node_modules/coveralls/bin/coveralls.js
node_js:
Expand Down
126 changes: 123 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,131 @@
# MobX-React Changelog

### 4.0.0

#### `observer` now uses shallow comparision for all props _(Breaking change)_

`observer` used to compare all properties shallow in the built-in _shouldComponentUpdate_, except when it received
non-observable data structures.
Because mobx-react cannot know whether a non observable has been deeply modified, it took no chances and just re-renders.

However, the downside of this when an unchanged, non-observable object is passed in to an observer component again, it would still cause a re-render.
Objects such as styling etc. To fix this mobx-react will now always compare all properties in a pure manner.
In general this should cause no trouble, as typically mutable data in mobx based objects is captured in observable objects, which will still cause components to re-render if needed.

If you need to pass in a deeply modified object and still want to make sure to cause a re-render, either

* make sure the object / array is an observable
* do not decorate your component with `observer`, but use `Observer` regions instead (see below)

See [#160](https://github.com/mobxjs/mobx-react/issues/160) for more details.

#### `inject(fn, component)` will now track `fn` as well

`inject(func)` is now reactive as well, that means that transformations in the selector function will be tracked, see [#111](https://github.com/mobxjs/mobx-react/issues/111)

```javascript
const NameDisplayer = ({ name }) => <h1>{name}</h1>

const UserNameDisplayer = inject(
stores => ({
name: stores.userStore.name
}),
NameDisplayer
)

const user = mobx.observable({
name: "Noa"
})

const App = () => (
<Provider userStore={user}>
<UserNameDisplayer />
</Provider>
)

ReactDOM.render(<App />, document.body)
```

_N.B. note that in this specific case NameDisplayer doesn't have to be an `observer`, as it doesn't receive observables, but just plain data from the transformer function._

#### `this.props` and `this.state` in React components are now observables as well

A common cause of confusion were cases like:

```javascript
@observer class MyComponent() {
@computed upperCaseName() {
return this.props.user.name.toUpperCase()
}

render() {
return <h1>{this.upperCaseName}</h1>
}
}
```

This component would re-render if `user.name` was modified, but it would still render the previous user's name if a complete new user was received!
The reason for that is that in the above example the only observable tracked by the computed value is `user.name`, but not `this.props.user`.
So a change to the first would be picked up, but a change in `props` itself, assigning a new user, not.

Although this is technically correct, it was a source of confusion.
For that reason `this.state` and `this.props` are now automatically converted to observables in any `observer` based react component.
For more details, see [#136](https://github.com/mobxjs/mobx-react/pull/136) by @Strate

#### Better support for Server Side Rendering

Introduced `useStaticRendering(boolean)` to better support server-side rendering scenarios. See [#140](https://github.com/mobxjs/mobx-react/issues/140)

#### Introduced `Observer` as alternative syntax to the `observer` decorator.

_This feature is still experimental and might change in the next minor release, or be deprecated_

Introduced `Observer`. Can be used as alternative to the `observer` decorator. Marks a component region as reactive.
See the Readme / [#138](https://github.com/mobxjs/mobx-react/issues/138)
Example:

```javascript
const UserNameDisplayer = ({ user }) => (
<Observer>
{() => <div>{user.name}</div>}
</Observer>
)
```

#### Using `observer` to inject stores is deprecated

The fact that `observer` could inject stores as well caused quite some confusion.
Because in some cases `observer` would return the original component (when not inject), but it would return a HoC when injecting.
To make this more consistent, you should always use `inject` to inject stores into a component. So use:

```
@inject("store1", "store2") @observer
class MyComponent extends React.Component {
```

or:

```
const MyComponent = inject("store1", "store2")(observer(props => rendering))
```

For more info see the related [discussion](https://github.com/mobxjs/mobx-react/commit/666577b41b7af8209839e7b243064a31c9951632#commitcomment-19773706)

#### Other improvements

* If `mobx` and `mobx-react` are used in combination, all reactions are run as part of React's batched updates. This minimizes the work of the reconciler, guarantees optimal rendering order of components (if the rendering was not triggered from within a React event). Tnx @gkaemmer for the suggestion.
* It is now possible to directly define `propTypes` and `defaultProps` on components wrapped with `inject` (or `observer(["stores"])`) again, see #120, #142. Removed the warnings for this, and instead improved the docs.
* Clean up data subscriptions if an error is thrown by an `observer` component, see [#134](https://github.com/mobxjs/mobx-react/pull/134) by @andykog
* export `PropTypes` as well in typescript typings, fixes #153
* Add react as a peer dependency
* Added minified browser build: `index.min.js`, fixes #147
* Generate better component names when using `inject`

---

### 3.5.9

* Introduced `useStaticRendering(boolean)` to better support server-side rendering scenerios. See [#140](https://github.com/mobxjs/mobx-react/issues/140)
* Print warning when `inject` and `observer` are used in the wrong order, see #146, by @delaetthomas
* export `PropTypes` as well, fixes #153
* Add react as a peer dependency

### 3.5.8

Expand Down
117 changes: 107 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ const TodoView = observer(class TodoView extends React.Component {
const TodoView = observer(({todo}) => <div>{todo.title}</div>)
```

### `Observer`

_This feature is still experimental and might change in the next minor release, or be deprecated_

`Observer` is a React component, which applies `observer` to an unanymous region in your component.
It takes as children a single, argumentless function which should return exactly one React component.
The rendering in the function will be tracked and automatically be re-rendered when needed.
This can come in handy when needing to pass render function to external components (for example the React Native listview), or if you
dislike the `observer` decorator / function.

Example:

```
class App extends React.Component {
render() {
return (
<div>
{this.props.person.name}
<Observer>
{() => <div>{this.props.person.name}</div>}
</Observer>
</div>
)
}
}

const person = observable({ name: "John" })

React.render(<App person={person} />, document.body)
person.name = "Mike" // will cause the Observer region to re-render
```

### Server Side Rendering with `useStaticRendering`

When using server side rendering, normal lifecycle hooks of React components are not fired, as the components are rendered only once.
Expand All @@ -99,7 +131,10 @@ TL;DR: the conceptual distinction makes a lot of sense when using MobX as well,
### About `shouldComponentUpdate`

It is possible to set a custom `shouldComponentUpdate`, but in general this should be avoid as MobX will by default provide a highly optimized `shouldComponentUpdate` implementation, based on `PureRenderMixin`.
If a custom `shouldComponentUpdate` is provided, it is consulted when the props changes (because the parent passes new props) or the state changes (as a result of calling `setState`), but if an observable used by the rendering is changed, the component will be re-rendered and `shouldComponent` is not consulted.
If a custom `shouldComponentUpdate` is provided, it is consulted when the props changes (because the parent passes new props) or the state changes (as a result of calling `setState`),
but if an observable used by the rendering is changed, the component will be re-rendered and `shouldComponent` is not consulted.

Since version 4, `mobx-react` will no longer trigger a re-rendering for non-observable objects that have been deeply changed.

### `componentWillReact` (lifecycle hook)

Expand Down Expand Up @@ -209,22 +244,81 @@ var Button = inject("color")(observer(({ color }) => {
}))
```

#### Omitting inject
If you are using `inject` with just store names, and there are no other (third party) decorators on the same component,
you can pass the store names directly to `observer` as well, which will create an inject under the hood. (Mind the array notation!)
#### Customizing inject

Instead of passing a list of store names, it is also possible to create a custom mapper function and pass it to inject.
The 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,
that are mapped into the original:

`mapperFunction: (allStores, props, context) => additionalProps`

Since version 4.0 the `mapperFunction` itself is tracked as well, so it is possible to do things like:

```javascript
var Button = observer(["color"], ({ color }) => {
/* ... etc ... */
}))
const NameDisplayer = ({ name }) => <h1>{name}</h1>

const UserNameDisplayer = inject(
stores => ({
name: stores.userStore.name
}),
NameDisplayer
)

@observer(["session", "theme"])
class MyComponent extends React.Component {
// etc
const user = mobx.observable({
name: "Noa"
})

const App = () => (
<Provider userStore={user}>
<UserNameDisplayer />
</Provider>
)

ReactDOM.render(<App />, document.body)
```

_N.B. note that in this *specific* case neither `NameDisplayer` or `UserNameDisplayer` doesn't need to be decorated with `observer`, since the observable dereferencing is done in the mapper function_

#### Using `propTypes` and `defaultProps` and other static properties in combination with `inject`

Inject wraps a new component around the component you pass into it.
This means that assigning a static property to the resulting component, will be applied to the HoC, and not to the original component.
So if you take the following example:

```javascript
const UserName = inject("userStore", ({ userStore, bold }) => someRendering())

UserName.propTypes = {
bold: PropTypes.boolean.isRequired,
userStore: PropTypes.object.isRequired // will always fail
}
```

The above propTypes are incorrect, `bold` needs to be provided by the caller of the `UserName` component and is checked by React.
However, `userStore` does not need to be required! Although it is required for the original stateless function component, it is not
required for the resulting inject component. After all, the whole point of that component is to provide that `userStore` itself.

So if you want to make assertions on the data that is being injected (either stores or data resulting from a mapper function), the propTypes
should be defined on the _wrapped_ component. Which is available through the static property `wrappedComponent` on the inject component:

```javascript
const UserName = inject("userStore", ({ userStore, bold }) => someRendering())

UserName.propTypes = {
bold: PropTypes.boolean.isRequired // could be defined either here ...
}

UserName.wrappedComponent.propTypes = {
// ... or here
userStore: PropTypes.object.isRequired // correct
}
```

The same principle applies to `defaultProps` and other static React properties.
Note that it is not allowed to redefine `contextTypes` on `inject` components (but is possible to define it on `wrappedComponent`)

Finally, mobx-react will automatically move non React related static properties from wrappedComponent to the inject component so that all static fields are
actually available to the outside world without needing `.wrappedComponent`.

#### Strongly typing inject

Expand Down Expand Up @@ -263,6 +357,9 @@ const mountedComponent = mount(
)
```

Bear in mind that using shallow rendering won't provide any useful results when testing injected components; only the injector will be rendered.
To test with shallow rendering, instantiate the `.wrappedComponent instead:`: `shallow(<Person.wrappedComponent />)`

## FAQ

**Should I use `observer` for each component?**
Expand Down
25 changes: 16 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobx-react",
"version": "3.5.9",
"version": "4.0.0-rc.4",
"description": "React bindings for MobX. Create fully reactive components.",
"main": "index.js",
"typings": "index",
Expand All @@ -9,11 +9,15 @@
"url": "https://github.com/mobxjs/mobx-react.git"
},
"scripts": {
"test": "npm run build && npm run test:console",
"test:ts": "tsc -p test/ts",
"test:build": "browserify -x react/addons -x react-native -x react/lib/ReactContext -x react/lib/ExecutionEnvironment test/*.js -o ./test/browser/test_bundle.js -t [ babelify --presets [ es2015 react ] --plugins [ transform-decorators-legacy transform-class-properties ] ]",
"test:travis": "webpack && browserify -x react-native -x react/addons -x react/lib/ReactContext -x react/lib/ExecutionEnvironment test/*.js -t [ babelify --presets [ es2015 react ] --plugins [ transform-decorators-legacy transform-class-properties ] ] | tape-run && npm run test:ts",
"test:console": "browserify -x react-native -x react/addons -x react/lib/ReactContext -x react/lib/ExecutionEnvironment test/*.js -t [ babelify --presets [ es2015 react ] --plugins [ transform-decorators-legacy transform-class-properties ] ] | tape-run | tap-spec && npm run test:ts",
"test:open": " open ./test/browser/index.html",
"test:browser": "npm run test:build && npm run test:open",
"build": "webpack && cp src/index.d.ts index.d.ts && cp src/index.d.ts native.d.ts && cp src/index.d.ts custom.d.ts",
"prepublish": "npm run build",
"test": "webpack && browserify -x react/addons -x react/lib/ReactContext -x react/lib/ExecutionEnvironment test/*.js | tape-run | tap-spec && tsc -p test/ts",
"travis": "webpack && browserify -x react/addons -x react/lib/ReactContext -x react/lib/ExecutionEnvironment test/*.js | tape-run && tsc -p test/ts",
"debug": "webpack && browserify -x react/addons -x react/lib/ReactContext -x react/lib/ExecutionEnvironment test/*.js | tape-run --browser chrome | tap-spec"
"prepublish": "npm run build"
},
"author": "Michel Weststrate",
"license": "MIT",
Expand All @@ -22,20 +26,23 @@
},
"homepage": "https://mobxjs.github.io/mobx",
"peerDependencies": {
"mobx": "^2.2.0",
"mobx": "^2.6.3",
"react": "^0.13.0 || ^0.14.0 || ^15.0.0"
},
"devDependencies": {
"babel-core": "^6.11.4",
"babel-loader": "^6.2.4",
"babel-plugin-transform-class-properties": "^6.11.5",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.16.0",
"babelify": "^7.3.0",
"browserify": "^13.1.0",
"empty-module": "0.0.2",
"enzyme": "^2.3.0",
"enzyme": "^2.6.0",
"jquery": "^2.1.4",
"lodash": "^4.0.1",
"mobx": "^2.6.0",
"mobx": "^2.6.3",
"react": "^15.2.0",
"react-addons-test-utils": "^15.2.0",
"react-dom": "^15.2.0",
Expand All @@ -56,4 +63,4 @@
"reactjs",
"reactive"
]
}
}
4 changes: 4 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export class Provider extends React.Component<any, {}> {

}

export class Observer extends React.Component<{ children?: () => React.ReactElement<any> }, {}> {

}

/**
* Enable dev tool support, makes sure that renderReport emits events.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import mobx from 'mobx';
import React from 'react';
import {unstable_batchedUpdates as rdBatched} from 'react-dom';
import {unstable_batchedUpdates as rnBatched} from 'react-native';

let TARGET_LIB_NAME;
if (__TARGET__ === 'browser') TARGET_LIB_NAME = 'mobx-react';
Expand All @@ -11,8 +13,14 @@ if (!mobx)
if (!React)
throw new Error(TARGET_LIB_NAME + ' requires React to be available');

if (__TARGET__ === 'browser' && typeof rdBatched === "function")
mobx.extras.setReactionScheduler(rdBatched);
if (__TARGET__ === 'native' && typeof rnBatched === "function")
mobx.extras.setReactionScheduler(rnBatched);

export {
observer,
Observer,
renderReporter,
componentByNodeRegistery,
trackComponents,
Expand Down
Loading