Skip to content

Commit

Permalink
TC 39 decorators (#3790)
Browse files Browse the repository at this point in the history
* add support for Stage3/2022.3 decorators

* bump Jest stack

* Update docs/enabling-decorators.md

Co-authored-by: Rick Pastoor <rickpastoor@gmail.com>

* bump from TS beta to 5.0.2

* rebase unto React 18 changes

* fix some decorator documentation issues

* bump test GC timeout

* try fix GC test

* v6.11.0-pre

* Doc improvements

* error on on missing `accessor` fields instead of silently ignoring

* fix lint error

* docs: added Babel decorator setup

* added class decorator

* add changelog entry for mobx-react

* minor readme improvements

* Removed dead code

---------

Co-authored-by: Ethan Knapp <ethan@matchlighter.net>
Co-authored-by: Matchlighter <ethank12345@gmail.com>
Co-authored-by: Rick Pastoor <rickpastoor@gmail.com>
  • Loading branch information
4 people authored Nov 11, 2023
1 parent 807c68a commit c926097
Show file tree
Hide file tree
Showing 63 changed files with 3,523 additions and 2,771 deletions.
8 changes: 8 additions & 0 deletions .changeset/afraid-cooks-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"mobx": minor
---

Added support for modern 2022.3 Decorators. [#3790](https://github.com/mobxjs/mobx/pull/3790)
* [Installation / usage instruction](https://mobx.js.org/enabling-decorators.html).
* [Introduction announcement](https://michel.codes/blogs/mobx-decorators)
* Original PR by [@Matchlighter](https://github.com/Matchlighter) in [#3638](https://github.com/mobxjs/mobx/pull/3638),
8 changes: 8 additions & 0 deletions .changeset/little-dancers-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"mobx-react": minor
---

Added support for modern 2022.3 Decorators. [#3790](https://github.com/mobxjs/mobx/pull/3790)
* [Installation / usage instruction](https://mobx.js.org/enabling-decorators.html).
* [Introduction announcement](https://michel.codes/blogs/mobx-decorators)
* Original PR by [@Matchlighter](https://github.com/Matchlighter) in [#3638](https://github.com/mobxjs/mobx/pull/3638),
1 change: 1 addition & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: {}

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ MobX is made possible by the generosity of the sponsors below, and many other [i

_Anything that can be derived from the application state, should be. Automatically._

MobX is a battle-tested library that makes state management simple and scalable by transparently applying functional reactive programming.
MobX is a signal based, battle-tested library that makes state management simple and scalable by transparently applying functional reactive programming.
The philosophy behind MobX is simple:

<div class="benefits">
Expand Down
6 changes: 1 addition & 5 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ _Simple, scalable state management._

---

> Documentation for the older **unsupported** V4/V5 can be [found here](https://github.com/mobxjs/mobx/blob/mobx4and5/docs/README.md), but be sure to read about the [current documentation first](https://mobx.js.org/about-this-documentation.html).
---

MobX is made possible by the generosity of the sponsors below, and many other [individual backers](backers-sponsors.md#backers). Sponsoring directly impacts the longevity of this project.

**🥇 Gold sponsors (\$3000+ total contribution):** <br/>
Expand Down Expand Up @@ -59,7 +55,7 @@ MobX is made possible by the generosity of the sponsors below, and many other [i

_Anything that can be derived from the application state, should be. Automatically._

MobX is a battle-tested library that makes state management simple and scalable by transparently applying functional reactive programming.
MobX is a signal based, battle-tested library that makes state management simple and scalable by transparently applying functional reactive programming.
The philosophy behind MobX is simple:

<div class="benefits">
Expand Down
19 changes: 19 additions & 0 deletions docs/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Usage:
- `action` _(annotation)_
- `action(fn)`
- `action(name, fn)`
- `@action` _(method / field decorator)_

All applications have actions. An action is any piece of code that modifies the state. In principle, actions always happen in response to an event. For example, a button was clicked, some input changed, a websocket message arrived, etc.

Expand Down Expand Up @@ -50,6 +51,23 @@ class Doubler {
}
```

<!--@action-->

```javascript
import { observable, action } from "mobx"

class Doubler {
@observable accessor value = 0

@action
increment() {
// Intermediate states will not become visible to observers.
this.value++
this.value++
}
}
```

<!--makeAutoObservable-->

```javascript
Expand Down Expand Up @@ -399,6 +417,7 @@ Usage:

- `flow` _(annotation)_
- `flow(function* (args) { })`
- `@flow` _(method decorator)_

The `flow` wrapper is an optional alternative to `async` / `await` that makes it easier to
work with MobX actions.
Expand Down
8 changes: 4 additions & 4 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ It is possible to use `extendObservable` to add observable fields to an existing

### `observable`

Usage: `observable(source, overrides?, options?)` or `observable` _(annotation)_
Usage: `observable(source, overrides?, options?)`, `observable` _(annotation)_ or `@observable accessor` _(field decorator)_.
<small>(<b>[further information](observable-state.md#observable)</b>)</small>

Clones an object and makes it observable. Source can be a plain object, array, Map or Set. By default, `observable` is applied recursively. If one of the encountered values is an object or array, that value will be passed through `observable` as well.
Expand Down Expand Up @@ -172,7 +172,7 @@ _An action is any piece of code that modifies the state._

### `action`

Usage: `action(fn)` or `action` _(annotation)_
Usage: `action(fn)`, `action` _(annotation)_ or `@action` _(method / field decorator)_
<small>(<b>[further information](actions.md)</b>)</small>

Use on functions that intend to modify the state.
Expand All @@ -186,7 +186,7 @@ Create a one-time action that is immediately invoked.

### `flow`

Usage: `flow(fn)` or `flow` _(annotation)_
Usage: `flow(fn)`, `flow` _(annotation)_ or `@flow` _(generator method decorator)_
<small>(<b>[further information](actions.md#using-flow-instead-of-async--await-)</b>)</small>

MobX friendly replacement for `async` / `await` that supports cancellation.
Expand All @@ -207,7 +207,7 @@ _Computed values can be used to derive information from other observables._

### `computed`

Usage: `computed(fn, options?)` or `computed(options?)` _(annotation)_
Usage: `computed(fn, options?)`, `computed(options?)` _(annotation)_ or `@computed` _(getter decorator)_
<small>(<b>[further information](computeds.md)</b>)</small>

Creates an observable value that is derived from other observables, but won't be recomputed unless one of the underlying observables changes.
Expand Down
2 changes: 2 additions & 0 deletions docs/computeds.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Usage:
- `computed` _(annotation)_
- `computed(options)` _(annotation)_
- `computed(fn, options?)`
- `@computed` _(getter decorator)_
- `@computed(options)` _(getter decorator)_

Computed values can be used to derive information from other observables.
They evaluate lazily, caching their output and only recomputing if one of the underlying observables has changed.
Expand Down
138 changes: 88 additions & 50 deletions docs/enabling-decorators.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,79 @@
---
title: Enabling decorators
sidebar_label: Enabling decorators {🚀}
title: Decorators
sidebar_label: Decorators {🚀}
hide_title: true
---

<script async type="text/javascript" src="//cdn.carbonads.com/carbon.js?serve=CEBD4KQ7&placement=mobxjsorg" id="_carbonads_js"></script>

# Enabling decorators {🚀}
# Decorators

MobX before version 6 encouraged the use of ES.next decorators to mark things as `observable`, `computed` and `action`. However, decorators are currently not an ES standard, and the process of standardization is taking a long time. It also looks like the standard will be different from the way decorators were implemented previously. In the interest of compatibility we have chosen to move away from them in MobX 6, and recommend the use of [`makeObservable` / `makeAutoObservable`](observable-state.md) instead.
## Enabling decorators

But many existing codebases use decorators, and a lot of the documentation and tutorial material online uses them as well. The rule is that anything you can use as an annotation to `makeObservable`, such as `observable`, `action` and `computed`, you can also use as a decorator. So let's examine what that looks like:
After years of alterations, ES decorators have finally reached Stage 3 in the TC39 process, meaning that they are quite stable and won't undergo breaking changes again like the previous decorator proposals have. MobX has implemented support for this new "2022.3/Stage 3" decorator syntax.
With modern decorators, it is no longer needed to call `makeObservable` / `makeAutoObservable`.

2022.3 Decorators are supported in:
* TypeScript (5.0 and higher, make sure that the `experimentalDecorators` flag is NOT enabled). [Example commit](https://github.com/mweststrate/currencies-demo/commit/acb9ac8c148e8beef88042c847bb395131e85d60).
* For Babel make sure the plugin [`proposal-decorators`](https://babeljs.io/docs/babel-plugin-proposal-decorators) is enabled with the highest version (currently `2023-05`). [Example commit](https://github.com/mweststrate/currencies-demo/commit/4999d2228208f3e1e10bc00a272046eaefde8585).

```js
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": false /* or just remove the flag */
}
}

// babel.config.json (or equivalent)
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"version": "2023-05"
}
]
]
}
```

## Using decorators

```javascript
import { observable, computed, action } from "mobx"

class Todo {
id = Math.random()
@observable accessor title = ""
@observable accessor finished = false

@action
toggle() {
this.finished = !this.finished
}
}

class TodoList {
@observable accessor todos = []

@computed
get unfinishedTodoCount() {
return this.todos.filter(todo => !todo.finished).length
}
}
```

Notice the usage of the new `accessor` keyword when using `@observable`.
It is part of the 2022.3 spec and is required if you want to use modern decorators.

<details id="legacy-decorators"><summary>Using legacy decorators</summary>

We do not recommend codebases to use TypeScript / Babel legacy decorators since they well never become an official part of the language, but you can still use them. It does require a specific setup for transpilation:

MobX before version 6 encouraged the use of legacy decorators and mark things as `observable`, `computed` and `action`.
While MobX 6 recommends against using these decorators (and instead use either modern decorators or [`makeObservable` / `makeAutoObservable`](observable-state.md)), it is in the current major version still possible.
Support for legacy decorators will be removed in MobX 7.

```javascript
import { makeObservable, observable, computed, action } from "mobx"
Expand Down Expand Up @@ -43,62 +106,37 @@ class TodoList {
}
}
```
</details>

MobX before version 6 did not require the `makeObservable(this)` call in the constructor, but because it makes the implementation of decorator simpler and more compatible, it now does. This instructs MobX to make the instances observable following the information in the decorators – the decorators take the place of the second argument to `makeObservable`.
<details id="migrate-decorators"><summary>Migrating from legacy decorators</summary>

We intend to continue to support decorators in this form.
Any existing MobX 4/5 codebase can be migrated to use `makeObservable` calls by our [code-mod](https://www.npmjs.com/package/mobx-undecorate).
When migrating from MobX 4/5 to 6, we recommend to always run the code-mod, to make sure the necessary `makeObservable` calls are generated.
To migrate from legacy decorators to modern decorators, perform the following steps:

Check out the [Migrating from MobX 4/5 {🚀}](migrating-from-4-or-5.md) section.

## Using `observer` as a decorator
1. Disable / remove the `experimentalDecorators` flag from your TypeScript configuration (or Babel equivalent)
2. Remove all `makeObservable(this)` calls from class constructors that use decorators.
3. Replace all instances of `@observable` (and variations) with `@observable accessor`

The `observer` function from `mobx-react` is both a function and a decorator that can be used on class components:

```javascript
@observer
class Timer extends React.Component {
/* ... */
}
```
</details>

## How to enable decorator support
<details id="gotchas"><summary>Decorator changes / gotchas</summary>

We do not recommend new codebases that use MobX use decorators until the point when they become an official part of the language, but you can still use them. It does require setup for transpilation so you have to use Babel or TypeScript.
MobX' 2022.3 Decorators are very similar to the MobX 5 decorators, so usage is mostly the same, but there are some gotchas:

### TypeScript
- `@observable accessor` decorators are _not_ enumerable. `accessor`s do not have a direct equivalent in the past - they're a new concept in the language. We've chosen to make them non-enumerable, non-own properties in order to better follow the spirit of the ES language and what `accessor` means.
The main cases for enumerability seem to have been around serialization and rest destructuring.
- Regarding serialization, implicitly serializing all properties probably isn't ideal in an OOP-world anyway, so this doesn't seem like a substantial issue (consider implementing `toJSON` or using `serializr` as possible alternatives)
- Addressing rest-destructuring, such is an anti-pattern in MobX - doing so would (likely unwantedly) touch all observables and make the observer overly-reactive).
- `@action some_field = () => {}` was and is valid usage (_if_ `makeObservable()` is also used). However, `@action accessor some_field = () => {}` is never valid.

Enable the compiler option `"experimentalDecorators": true` and `"useDefineForClassFields": true` in your `tsconfig.json`.
</details>

### Babel 7
## Using `observer` as a decorator

Install support for decorators: `npm i --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators`. And enable it in your `.babelrc` file (note that the order is important):
The `observer` function from `mobx-react` is both a function and a decorator that can be used on class components:

```javascript
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": false }]
// In contrast to MobX 4/5, "loose" must be false! ^
]
@observer
class Timer extends React.Component {
/* ... */
}
```

### Decorator syntax and Create React App (v2)

Decorators are only supported out of the box when using TypeScript in `create-react-app@^2.1.1` and newer. In older versions or when using vanilla JavaScript use eject, or the [customize-cra](https://github.com/arackaf/customize-cra) package.

## Disclaimer: Limitations of the decorator syntax

The current transpiler implementations of the decorator syntax are quite limited and don't behave exactly the same.
Also, many compositional patterns are currently not possible with decorators, until the stage-2 proposal has been implemented by all transpilers.
For this reason the scope of decorator syntax support in MobX is currently scoped to make sure that the supported features
behave consistently across all environments.

The following patterns are not officially supported by the MobX community:

- Redefining decorated class members in inheritance trees
- Decorating static class members
- Combining decorators provided by MobX with other decorators
- Hot module reloading (HMR) / React-hot-loader might not work as expected
29 changes: 12 additions & 17 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,26 @@ There are two types of React bindings, `mobx-react-lite` supports only functiona

**CDN:** https://cdnjs.com/libraries/mobx / https://unpkg.com/mobx/dist/mobx.umd.production.min.js

# Transpilation settings

## MobX and Decorators

Based on your preference, MobX can be used with or without decorators.
Both the legacy implementation and the standardised TC-39 version of decorators are currently supported.
See [enabling-decorators.md](enabling-decorators.md) for more details on how to enable them.
Legacy decorator support will be removed in MobX 7, in favor of the standard.

## Use spec compliant transpilation for class properties

⚠️ **Warning:** When using MobX with TypeScript or Babel, and you plan to use classes; make sure to update your configuration to use a TC-39 spec compliant transpilation for class fields, since this is not the default. Without this, class fields cannot be made observable before they are initialized.
When using MobX with TypeScript or Babel, and you plan to use classes; make sure to update your configuration to use a TC-39 spec compliant transpilation for class fields, since this is not always the default. Without this, class fields cannot be made observable before they are initialized.

- **TypeScript**: Set the compiler option `"useDefineForClassFields": true`.
- **Babel**: Make sure to use at least version 7.12, with the following configuration:
```json
{
// Babel < 7.13.0
"plugins": [["@babel/plugin-proposal-class-properties", { "loose": false }]],

// Babel >= 7.13.0 (https://babeljs.io/docs/en/assumptions)
"plugins": [["@babel/plugin-proposal-class-properties"]],
"assumptions": {
Expand All @@ -53,21 +62,7 @@ import { configure } from "mobx"
configure({ useProxies: "never" }) // Or "ifavailable".
```

## MobX and Decorators

If you have used MobX before, or if you followed online tutorials, you probably saw MobX with decorators like `@observable`.
In MobX 6, we have chosen to move away from decorators by default, for maximum compatibility with standard JavaScript.
They can still be used if you [enable them](enabling-decorators.md) though.

## Development vs production

Unless you're using pre-build distribution ending with `.[production|development].min.js`, Mobx uses `process.env.NODE_ENV` variable to detect the environment. Make sure it's set to `"production"` on production. This is usually done by your favourite bundler:
[webpack](https://reactjs.org/docs/optimizing-performance.html#webpack)
[Rollup](https://reactjs.org/docs/optimizing-performance.html#rollup)
[Browserify](https://reactjs.org/docs/optimizing-performance.html#browserify)
[Brunch](https://reactjs.org/docs/optimizing-performance.html#brunch)

Most of the safety checks, like [`enforceAction`](https://mobx.js.org/configuration.html#enforceactions) and similar, happens on development only.
This option will be removed in MobX 7.

## MobX on other frameworks / platforms

Expand Down
Loading

0 comments on commit c926097

Please sign in to comment.