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

Commit

Permalink
Breaking version 2.0 (#212)
Browse files Browse the repository at this point in the history
* Schedule uncommitted reaction cleanup (#121)

* Test cleaning up reactions for uncommitted components

* First attempt at a fix for cleaning up reactions from uncommitted components

* Use debounce instead of fixed interval

* Add unit test for missing observable changes before useEffect runs

This is a test to check that observable changes made between the
first component render and commit are not lost.

It currently fails (and did so before the change in PR #119)

* Add test for cleanup timer firing too early for some components

This demonstrates (in a slightly contrived way) how, if the
cleanup timer fires between a recent component being
rendered and it being committed, that it would incorrectly
tidy up a reaction for a soon-to-be-committed component.

* Update test for missing changes to check Strict and non-Strict mode

We had an existing test to check that observable changes between render
and commit didn't go missing, but it only checked in Strict mode, and
there's a problem with non-Strict mode.

* Add cleanup tracking and more tests

This adds full cleanup tracking, and even more tests:

- we now track how long ago potentially leaked reactions
were created, and only clean those that were leaked 'a while ago'
- if a reaction is incorrectly disposed because a component
went away for a very long time and came back again later
(in a way React doesn't even do right now), we safely recreate it and re-render
- trap the situation where a change is made to a tracked observable
between first render and commit (where we couldn't force an update
because we hadn't _been_ committed) and force a re-render
- more unit tests

* Fix renamed test file

When I renamed this file, I forgot the .test. suffix. D'oh.

* Extract tracking and cleanup logic out to separate file

* Update src/useObserver.ts

Co-Authored-By: RoystonS <[email protected]>

* Move some more tracking internals into the tracking code

* 2.0.0-alpha.0

* 2.0.0-alpha.1

* 2.0.0-alpha.2

* Upgrade to React 16.9

* Add dedup script and run it

* Remove deprecated hooks

* Increase size limit

* Remove note about Next version

* Remove unused productionMode util

* Improve readme

* Pin dependencies

* Merge master properly

* Ignore build cache files

* Revert removal of tsdx dep

* Remove .browserlistrc

* 2.0.0-alpha.3

* Bundling need to use build tsconfig

* 2.0.0-alpha.4

* Remove object destructuring from optimizeForReactDOM/Native (#240)

* Preserve generics when using `observer` (#244)

* Preserve generics when using `observer`

* Remove any casting from statics as it's no longer needed

* Re-add overloads for explicitly specifying props type

* Allow for passing options without `forwardRef`

* Merge new `observer` overloads

* Remove copy of UMD bundle

* 2.0.0-alpha.5

* Batched updates are mandatory

* Replace .npmignore with files field

* Fix tests

* Increase size limit

Co-authored-by: Royston Shufflebotham <[email protected]>
Co-authored-by: Renovate Bot <[email protected]>
Co-authored-by: Tarvo R <[email protected]>
Co-authored-by: Lukáš Novotný <[email protected]>
  • Loading branch information
5 people authored Apr 6, 2020
1 parent d638b07 commit ef91cca
Show file tree
Hide file tree
Showing 28 changed files with 862 additions and 768 deletions.
6 changes: 0 additions & 6 deletions .browserlistrc

This file was deleted.

29 changes: 0 additions & 29 deletions .npmignore

This file was deleted.

4 changes: 2 additions & 2 deletions .size-limit.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[
{
"path": "dist/mobxreactlite.umd.production.min.js",
"limit": "1.7 KB",
"limit": "1.8 KB",
"webpack": false,
"running": false
},
{
"path": "dist/mobxreactlite.cjs.production.min.js",
"limit": "1.7 KB",
"limit": "1.8 KB",
"webpack": false,
"running": false
}
Expand Down
193 changes: 39 additions & 154 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,46 @@
# mobx-react-lite <!-- omit in toc -->
# mobx-react-lite

[![CircleCI](https://circleci.com/gh/mobxjs/mobx-react-lite.svg?style=svg)](https://circleci.com/gh/mobxjs/mobx-react-lite)[![Coverage Status](https://coveralls.io/repos/github/mobxjs/mobx-react-lite/badge.svg)](https://coveralls.io/github/mobxjs/mobx-react-lite)
[![CircleCI](https://circleci.com/gh/mobxjs/mobx-react-lite.svg?style=svg)](https://circleci.com/gh/mobxjs/mobx-react-lite)[![Coverage Status](https://coveralls.io/repos/github/mobxjs/mobx-react-lite/badge.svg)](https://coveralls.io/github/mobxjs/mobx-react-lite)[![NPM downloads](https://img.shields.io/npm/dm/mobx-react-lite.svg?style=flat)](https://npmjs.com/package/mobx-react-lite)[![Minzipped size](https://img.shields.io/bundlephobia/minzip/mobx-react-lite.svg)](https://bundlephobia.com/result?p=mobx-react-lite)

[![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.svg?v=101)](https://github.com/ellerbrock/typescript-badges/)[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)

[![Join the chat at https://gitter.im/mobxjs/mobx](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mobxjs/mobx?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

This is a next iteration of [mobx-react](https://github.com/mobxjs/mobx-react) coming from introducing React hooks which simplifies a lot of internal workings of this package.
[![NPM](https://nodei.co/npm/mobx-react-lite.png)](https://www.npmjs.com/package/mobx-react-lite)

**You need React version 16.8.0 and above**

Class based components **are not supported** except using `<Observer>` directly in class `render` method. If you want to transition existing projects from classes to hooks (as most of us do), you can use this package alongside the [mobx-react](https://github.com/mobxjs/mobx-react) just fine. The only conflict point is about the `observer` HOC. Subscribe [to this issue](https://github.com/mobxjs/mobx-react/issues/640) for a proper migration guide.
This is a lighter version of [mobx-react](https://github.com/mobxjs/mobx-react) which supports React **functional components only** and as such makes the library slightly faster and smaller (_only 1.5kB gzipped_). In fact `mobx-react@6` has this library as a dependency and builds on top of it.

[![NPM](https://nodei.co/npm/mobx-react-lite.png)](https://www.npmjs.com/package/mobx-react-lite)
The library does not include any Provider/inject utilities as they can be fully replaced with [React Context](https://mobx-react.js.org/recipes-context). Check out [the migration guide](https://mobx-react.js.org/recipes-migration).

Project is written in TypeScript and provides type safety out of the box. No Flow Type support is planned at this moment, but feel free to contribute.
Class based components **are not supported** except using `<Observer>` directly in class `render` method. If you want to transition existing projects from classes to hooks, use [mobx-react 6+](https://github.com/mobxjs/mobx-react).

See more at [the libraries overview](https://mobx-react.js.org/libraries).

## User Guide 👉 https://mobx-react.js.org

The site contains various examples and recipes for using MobX in React world. Feel free to contribute. The API reference of this package follows 👇.

## API reference ⚒

> **`<Observer>{renderFn}</Observer>`** _([user guide](https://mobx-react.js.org/observer-component))_
### **`<Observer>{renderFn}</Observer>`** _([user guide](https://mobx-react.js.org/observer-component))_

Is a React component, which applies observer to an anonymous region in your component.

> **`observer<P>(baseComponent: FunctionComponent<P>, options?: IObserverOptions): FunctionComponent<P>`** _([user guide](https://mobx-react.js.org/observer-hoc))_
### **`observer<P>(baseComponent: FunctionComponent<P>, options?: IObserverOptions): FunctionComponent<P>`** _([user guide](https://mobx-react.js.org/observer-hoc))_

```ts
interface IObserverOptions {
// Pass true to use React.forwardRef over the inner component. It's false by the default.
// Pass true to wrap the inner component with React.forwardRef.
// It's false by the default.
forwardRef?: boolean
}
```

> **`useObserver<T>(fn: () => T, baseComponentName = "observed", options?: IUseObserverOptions): T`** _([user guide](https://mobx-react.js.org/observer-hook))_
The observer converts a component into a reactive component, which tracks which observables are used automatically and re-renders the component when one of these values changes.

### **`useObserver<T>(fn: () => T, baseComponentName = "observed", options?: IUseObserverOptions): T`** _([user guide](https://mobx-react.js.org/observer-hook))_

```ts
interface IUseObserverOptions {
Expand All @@ -40,167 +49,43 @@ interface IUseObserverOptions {
}
```

**`useLocalStore<T, S>(initializer: () => T, source?: S): T`** _([user guide](https://mobx-react.js.org/state-local))_

**`useAsObservableSource<T>(source: T): T`** _([user guide](https://mobx-react.js.org/state-outsourcing))_

## React Strict mode ☄
It allows you to use an observer like behaviour, but still allowing you to optimize the component in any way you want (e.g. using memo with a custom areEqual, using forwardRef, etc.) and to declare exactly the part that is observed (the render phase).

Feel free to try out `mobx-react-lite@next` which is based on latest 1.x, but contains experimental support for handling Concurrent mode in React properly.
### **`useLocalStore<T, S>(initializer: () => T, source?: S): T`** _([user guide](https://mobx-react.js.org/state-local))_

## Optimize rendering
Local observable state can be introduced by using the useLocalStore hook, that runs its initializer function once to create an observable store and keeps it around for a lifetime of a component.

[Check out the elaborate explanation](https://github.com/mobxjs/mobx-react-lite/issues/153#issuecomment-490511464).
### **`useAsObservableSource<T>(source: T): T`** _([user guide](https://mobx-react.js.org/state-outsourcing))_

If this is something that concerns you, we have prepared files you can simply import to configure MobX to use React batched updates depending on your platform.

**React DOM:**
The useAsObservableSource hook can be used to turn any set of values into an observable object that has a stable reference (the same object is returned every time from the hook).

> import 'mobx-react-lite/optimizeForReactDom'
## Observer batching

**React Native:**
[Check out the elaborate explanation](https://github.com/mobxjs/mobx-react/pull/787#issuecomment-573599793).

> import 'mobx-react-lite/optimizeForReactNative'
In short without observer batching the React doesn't guarantee the order component rendering in some cases. We highly recommend that you configure batching to avoid these random surprises.

Import one of these before any React rendering is happening, typically `index.js/ts`. For Jest tests you can utilize [setupFilesAfterEnv](https://jestjs.io/docs/en/configuration#setupfilesafterenv-array).

### Custom batched updates

Above imports are for a convenience. If you for some reason have customized version of batched updates, you can do the following instead.

```js
import { optimizeScheduler } from "mobx-react-lite"
optimizeScheduler(customBatchedUpdates)
```

## Deprecation notice ⚠

Following utilities are still available in the package, but they are deprecated and will be removed in the next major version (2.x). As such, they are not mentioned in the user guide and it's not recommend to continue using these.

---

### `useObservable<T>(initialValue: T): T`

> **Use the `useLocalStore` instead** ([user guide](https://mobx-react.js.org/state-local))
React hook that allows creating observable object within a component body and keeps track of it over renders. Gets all the benefits from [observable objects](https://mobx.js.org/refguide/object.html) including computed properties and methods. You can also use arrays, Map and Set.

Warning: With current implementation you also need to wrap your component to `observer`. It's also possible to have `useObserver` only in case you are not expecting rerender of the whole component.

```tsx
import { useObservable, useObserver } from "mobx-react-lite"

const TodoList = () => {
const todos = useObservable(new Map<string, boolean>())
const todoRef = React.useRef()
const addTodo = React.useCallback(() => {
todos.set(todoRef.current.value, false)
todoRef.current.value = ""
}, [])
const toggleTodo = React.useCallback((todo: string) => {
todos.set(todo, !todos.get(todo))
}, [])

return useObserver(() => (
<div>
{Array.from(todos).map(([todo, done]) => (
<div onClick={() => toggleTodo(todo)} key={todo}>
{todo}
{done ? "" : ""}
</div>
))}
<input ref={todoRef} />
<button onClick={addTodo}>Add todo</button>
</div>
))
}
```

#### Lazy initialization

Lazy initialization (similar to `React.useState`) is not available. In most cases your observable state should be a plain object which is cheap to create. With `useObserver` the component won't even rerender and state won't be recreated. In case you really want a more complex state or you need to use `observer`, it's very simple to use MobX directly.

```tsx
import { observer } from "mobx-react-lite"
import { observable } from "mobx"
import { useState } from "react"

const WithComplexState = observer(() => {
const [complexState] = useState(() => observable(new HeavyState()))
if (complexState.loading) {
return <Loading />
}
return <div>{complexState.heavyName}</div>
})
```

[![Edit TodoList](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/jzj48v2xry?module=%2Fsrc%2FTodoList.tsx)

Note that if you want to track a single scalar value (string, number, boolean), you would need [a boxed value](https://mobx.js.org/refguide/boxed.html) which is not recognized by `useObservable`. However, we recommend to just `useState` instead which gives you almost same result (with slightly different API).

### `useComputed(func: () => T, inputs: ReadonlyArray<any> = []): T`

> **Use the `useLocalStore` instead** ([user guide](https://mobx-react.js.org/state-local))
Another React hook that simplifies computational logic. It's just a tiny wrapper around [MobX computed](https://mobx.js.org/refguide/computed-decorator.html#-computed-expression-as-function) function that runs computation whenever observable values change. In conjuction with `observer` the component will rerender based on such a change.

```tsx
const Calculator = observer(({ hasExploded }: { hasExploded: boolean }) => {
const inputRef = React.useRef()
const inputs = useObservable([1, 3, 5])
const result = useComputed(
() => (hasExploded ? "💣" : inputs.reduce(multiply, 1) * Number(!hasExploded)),
[hasExploded]
)

return (
<div>
<input ref={inputRef} />
<button onClick={() => inputs.push(parseInt(inputRef.current.value) | 1)}>
Multiply
</button>
<div>
{inputs.join(" * ")} = {result}
</div>
</div>
)
})
```

Notice that since the computation depends on non-observable value, it has to be passed as a second argument to `useComputed`. There is [React `useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo) behind the scenes and all rules applies here as well except you don't need to specify dependency on observable values.
**React DOM:**

[![Edit Calculator](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/jzj48v2xry?module=%2Fsrc%2FCalculator.tsx)
> import 'mobx-react-lite/batchingForReactDom'
### `useDisposable<D extends TDisposable>(disposerGenerator: () => D, inputs: ReadonlyArray<any> = []): D`
**React Native:**

> **Use the `React.useEffect` instead** ([user guide](https://mobx-react.js.org/recipes-effects))
> import 'mobx-react-lite/batchingForReactNative'
The disposable is any kind of function that returns another function to be called on a component unmount to clean up used resources. Use MobX related functions like [`reaction`](https://mobx.js.org/refguide/reaction.html), [`autorun`](https://mobx.js.org/refguide/autorun.html), [`when`](https://mobx.js.org/refguide/when.html), [`observe`](https://mobx.js.org/refguide/observe.html), or anything else that returns a disposer.
Returns the generated disposer for early disposal.
### Opt-out

Example (TypeScript):
To opt-out from batching in some specific cases, simply import the following to silence the warning.

```typescript
import { reaction } from "mobx"
import { observer, useComputed, useDisposable } from "mobx-react-lite"
> import 'mobx-react-lite/batchingOptOut'
const Name = observer((props: { firstName: string; lastName: string }) => {
const fullName = useComputed(() => `${props.firstName} ${props.lastName}`, [
props.firstName,
props.lastName
])
### Custom batched updates

// when the name changes then send this info to the server
useDisposable(() =>
reaction(
() => fullName,
() => {
// send this to some server
}
)
)
Above imports are for a convenience to utilize standard versions of batching. If you for some reason have customized version of batched updates, you can do the following instead.

// render phase
return `Your full name is ${props.firstName} ${props.lastName}`
})
```js
import { observerBatching } from "mobx-react-lite"
observerBatching(customBatchedUpdates)
```
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions batchingOptOut.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("./dist").observerBatchingOptOut()
3 changes: 0 additions & 3 deletions empty.js

This file was deleted.

2 changes: 2 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
// should be to be in each test file to load typings properly
require("@testing-library/jest-dom/extend-expect")

global.__DEV__ = false
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobx-react-lite",
"version": "1.5.2",
"version": "2.0.0-alpha.5",
"description": "Lightweight React bindings for MobX based on React 16.8 and Hooks",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand All @@ -14,6 +14,10 @@
"type": "git",
"url": "https://github.com/mobxjs/mobx-react-lite.git"
},
"files": [
"dist/*",
"batching*"
],
"scripts": {
"prettier": "prettier --write \"./{src,test}/*.{js,ts,tsx}\"",
"lint": "eslint . --ext .js,.ts,.tsx",
Expand All @@ -24,7 +28,6 @@
"coverage": "jest --coverage",
"prebuild": "rimraf dist",
"build": "yarn bundle",
"postbuild": "shx cp dist/mobxreactlite.umd.production.min.js dist/index.min.js",
"bundle": "tsdx build --name mobxReactLite --format=cjs,esm,umd --tsconfig tsconfig.build.json",
"prepublishOnly": "yarn build",
"dedup": "npx yarn-deduplicate -s fewer yarn.lock"
Expand Down
1 change: 1 addition & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare const __DEV__: boolean
5 changes: 1 addition & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import "./assertEnvironment"

export { useObservable } from "./useObservable"
export { useComputed } from "./useComputed"
export { useDisposable } from "./useDisposable"
export { isUsingStaticRendering, useStaticRendering } from "./staticRendering"
export { observer, IObserverOptions } from "./observer"
export { useObserver, ForceUpdateHook, IUseObserverOptions } from "./useObserver"
export { Observer } from "./ObserverComponent"
export { useForceUpdate } from "./utils"
export { useAsObservableSource } from "./useAsObservableSource"
export { useLocalStore } from "./useLocalStore"
export { optimizeScheduler } from "./optimizeScheduler"
export { observerBatching, observerBatchingOptOut, isObserverBatched } from "./observerBatching"
19 changes: 19 additions & 0 deletions src/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,29 @@ export function observer<P extends object, TRef = {}>(
): React.MemoExoticComponent<
React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<TRef>>
>

export function observer<P extends object>(
baseComponent: React.FunctionComponent<P>,
options?: IObserverOptions
): React.FunctionComponent<P>

export function observer<
C extends React.FunctionComponent<any> | React.RefForwardingComponent<any>,
Options extends IObserverOptions
>(
baseComponent: C,
options?: Options
): Options extends { forwardRef: true }
? C extends React.RefForwardingComponent<infer TRef, infer P>
? C &
React.MemoExoticComponent<
React.ForwardRefExoticComponent<
React.PropsWithoutRef<P> & React.RefAttributes<TRef>
>
>
: never /* forwardRef set for a non forwarding component */
: C & React.FunctionComponent

// n.b. base case is not used for actual typings or exported in the typing files
export function observer<P extends object, TRef = {}>(
baseComponent: React.RefForwardingComponent<TRef, P>,
Expand Down
Loading

0 comments on commit ef91cca

Please sign in to comment.