diff --git a/.circleci/config.yml b/.circleci/config.yml index 3cf57f9275..a612f1509e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,10 +26,9 @@ jobs: steps: - attach_workspace: at: . + - run: yarn mobx build test - run: yarn lint - run: yarn test -i - - run: yarn mobx test:types - - run: yarn mobx-react test:types mobx: executor: node-executor @@ -46,13 +45,22 @@ jobs: - store_artifacts: path: packages/mobx/perf_report destination: mobx-perf + mobx-react: + executor: node-executor + steps: + - attach_workspace: + at: . + - run: yarn mobx build test + - run: yarn mobx test:size workflows: version: 2 - test-all: + main: jobs: - install - test: requires: [install] - mobx: requires: [install] + - mobx-react: + requires: [install] \ No newline at end of file diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index b70a898f60..4d06dee693 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -4,7 +4,7 @@ on: ["push", "pull_request"] jobs: mobx: - name: MobX Coverage + name: Packages coverage runs-on: ubuntu-latest steps: - name: Checkout Repo @@ -21,12 +21,13 @@ jobs: - name: Install Dependencies run: yarn --frozen-lockfile --ignore-scripts + - name: Build mobx + run: yarn mobx build --target test + - name: Run Coverage - run: yarn mobx test:coverage + run: yarn coverage - name: Upload to coveralls uses: coverallsapp/github-action@v1.1.2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./packages/mobx/coverage/lcov.info - base-path: packages/mobx diff --git a/jest.base.config.js b/jest.base.config.js index 7da7285bdf..ea3f2c8978 100644 --- a/jest.base.config.js +++ b/jest.base.config.js @@ -18,7 +18,8 @@ module.exports = function buildConfig(packageDirectory, pkgConfig) { }, testRegex: "__tests__/.*\\.(j|t)sx?$", coverageDirectory: "/coverage/", - collectCoverageFrom: ["/packages/*/src/**/*.{ts,tsx}"], + coverageReporters: ["lcov", "text"], + collectCoverageFrom: ["/src/**/*.{ts,tsx}", "!**/node_modules/**"], displayName: packageName, ...pkgConfig } diff --git a/jest.config.js b/jest.config.js index bb763c7c51..fb1ffb1f18 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,4 +2,5 @@ const buildConfig = require("./jest.base.config") module.exports = buildConfig(__dirname, { projects: ["/packages/*/jest.config.js"] + // collectCoverageFrom: ["/packages/*/src/**/*.{ts,tsx}"] }) diff --git a/package.json b/package.json index 124420cce3..c8e5d03e93 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,12 @@ ], "scripts": { "test": "jest", + "coverage": "jest --coverage", "lint": "eslint packages/*/src/**/*", "prettier": "prettier --write **/*.{js,ts,md}", "release": "yarn changeset publish", "mobx": "yarn workspace mobx", + "mobx-react": "yarn workspace mobx-react", "mobx-undecorate": "yarn workspace mobx-undecorate", "docs:build": "yarn --cwd website build", "docs:start": "yarn --cwd website start", @@ -33,7 +35,9 @@ "import-size": "^1.0.2", "iterall": "^1.3.0", "jest": "^26.6.2", + "jest-mock-console": "^1.0.1", "lint-staged": "^10.1.7", + "minimist": "^1.2.5", "mkdirp": "1.0.4", "prettier": "^2.0.5", "pretty-quick": "3.1.0", @@ -47,5 +51,5 @@ "hooks": { "pre-commit": "pretty-quick --staged" } - } + } } diff --git a/packages/mobx-react/.browserlistrc b/packages/mobx-react/.browserlistrc new file mode 100644 index 0000000000..915b680508 --- /dev/null +++ b/packages/mobx-react/.browserlistrc @@ -0,0 +1,6 @@ +{ + "targets": { + "chrome": "58", + "ie": "9" + } +} diff --git a/packages/mobx-react/CHANGELOG.md b/packages/mobx-react/CHANGELOG.md new file mode 100644 index 0000000000..0c95f33b44 --- /dev/null +++ b/packages/mobx-react/CHANGELOG.md @@ -0,0 +1,632 @@ +# mobx-react + +## 7.0.0 + +Release for compatibility with MobX v6 + +## 6.3.1 + +### Patch Changes + +- [`aa780c0`](https://github.com/mobxjs/mobx-react/commit/aa780c07162be99e198e7bbdbd6465c1f451f1d6) [#908](https://github.com/mobxjs/mobx-react/pull/908) Thanks [@FredyC](https://github.com/FredyC)! - Initial setup of [changesets](https://github.com/atlassian/changesets). No code changes present. + +## 6.3.0 + +- Updated mobx-react-lite to 2.2.0 which removes the need to manually configure batching. Fixes [#859](https://github.com/mobxjs/mobx-react/issues/859) + +## 6.2.4 + +- Fix error thrown in the already defined observer class component warning message when attempting to get the components display name. [#887](https://github.com/mobxjs/mobx-react/issues/887) + +## 6.2.3 + +- Log warning if class component is already an observer to prevent memory leaks. [#839](https://github.com/mobxjs/mobx-react/issues/839) +- Fix disposeOnUnmount when using react-hot-loader. [#725](https://github.com/mobxjs/mobx-react/issues/725) + +## 6.2.2 + +- Observer batching imports are kept in production builds as side effects ([see issue](https://github.com/mobxjs/mobx-react-lite/issues/273)) + +## 6.2.1 + +- Remove auto configured observer batching using react-dom. Fixes: [#852](https://github.com/mobxjs/mobx-react/issues/852). + +## 6.2.0 + +- Updated to latest mobx-react-lite V2 for compatibility with `React.StrictMode`. +- Observer batching (see more [in the docs](https://github.com/mobxjs/mobx-react-lite/#observer-batching)). +- Possibly breaking change, the `dist/mobxreact.rn.module.js` is no longer available, use `dist/mobxreact.esm.js` instead. + +## 6.1.6 / 6.1.7 + +- Fix an issue with class components & observableRequiresReaction. [#806](https://github.com/mobxjs/mobx-react/issues/806) through [#829](https://github.com/mobxjs/mobx-react/pull/829) +- Use TSDX for building to mitigate issues with accessing `process.env` [#821](https://github.com/mobxjs/mobx-react/pull/821) + +## 6.1.5 + +- Added check if `process.env` is available, fixes [#801](https://github.com/mobxjs/mobx-react/issues/801) through [#812](https://github.com/mobxjs/mobx-react/pull/812) by [@ynejati](https://github.com/ynejati) +- Added warning if component's `render` method is accidentally overwritten. [#799](https://github.com/mobxjs/mobx-react/pull/799) by [@Venryx](https://github.com/Venryx). Helps prevent memory leaks as in: [#797](https://github.com/mobxjs/mobx-react/issues/797) + +## 6.1.4 + +- Update dependency mobx-react-lite@1.4.2 which includes fix for [RN Fast Refresh](https://github.com/mobxjs/mobx-react-lite/issues/226) + +## 6.1.2 / 6.1.3 + +- Add reexport of `useObserver` from `mobx-react-lite` [#734](https://github.com/mobxjs/mobx-react/issues/734) +- Add the ability to pass multiple children to Provider +- Fixed [#717](https://github.com/mobxjs/mobx-react/issues/717). Now `inject` works correctly with components that use `React.forwardRef` +- Observer checks for use of React.memo [#720](https://github.com/mobxjs/mobx-react/issues/720) +- Get rid of the redundant Injector wrapper [#716](https://github.com/mobxjs/mobx-react/pull/716) + +## 6.1.1 + +- Fixed issue where combining `@disposeOnUnmount` with `disposeOnUnmount` didn't clean up everything. Fixes [#666](https://github.com/mobxjs/mobx-react/issues/666) trough [#671](https://github.com/mobxjs/mobx-react/pull/671) by [@JabX](https://github.com/JabX) + +## 6.1.0 + +- Restored the classic implementation of `observer`: class based components are patched again, rather than wrapping them in ``, see [#703](https://github.com/mobxjs/mobx-react/pull/703). Fixes: + - `componentDidUpdate` not being triggered after a reactive render [#692](https://github.com/mobxjs/mobx-react/issues/692) + - The appearance of an additional `` component in the component tree, which complicates shallow testing [#699](https://github.com/mobxjs/mobx-react/issues/699) + - Some regressions in `disposeOnUnmount` [#702](https://github.com/mobxjs/mobx-react/issues/702) + - Note that dev tool support, and other constraints mentioned in the 6.0.0 release notes have not been restored. +- The function `useStaticRendering(value: boolean): void` from mobx-react-lite is now exposed + +## 6.0.4 + +- Fixed IE 11 compatibility which was accidentally broken. Fixes [#698](https://github.com/mobxjs/mobx-react/issues/698) + +## 6.0.3 + +- `disposeOnUnmount` now supports initializing it with an array of disposers. Fixes [#637](https://github.com/mobxjs/mobx-react/pull/637) through [#641](https://github.com/mobxjs/mobx-react/pull/641) by [@Amareis](https://github.com/Amareis) +- Fixed hoisting of statically declared members. Fixes [#678](https://github.com/mobxjs/mobx-react/issues/678) through [#682](https://github.com/mobxjs/mobx-react/pull/682) by [@meabed](https://github.com/meabed) + +## 6.0.2 + +- Added missing types for `MobXProviderContext`, `useLocalStore` and `useAsObservableSource`. Fixes #679. + +## 6.0.0 + +**Breaking changes** + +- The minimal supported version of React is 16.8.0 +- Killed the possibility to directly pass store names to `observer`. Always use `inject` instead. (This was deprecated for a long time already). `observer(["a", "b"], component)` should now be written as `inject("a", "b")(component)`. +- `observer` components no longer automatically recover from errors (to prevent potential memory leaks). Instead, this is the responsibility of error boundaries. +- `inject` now supports ref forwarding. As such, the `.wrappedInstance` property has been removed since refs can be used instead. (Fixes [#616](https://github.com/mobxjs/mobx-react/issues/616) (See also [#619](https://github.com/mobxjs/mobx-react/pull/619) by [42shadow42](https://github.com/42shadow42)) +- Changing the set of stores in `Provider` is no longer supported and while throw a hard error (this was a warning before), as the model of `Provider` / `inject` has always been designed to inject final values into the tree. (That is, constanted references, the injected objects themselves can be stateful without problem). If you want to dynamically swap what is provided into the tree, use `React.createContext` instead of `Provider` / `inject`. The suppressChangedStoreWarning`flag for`Provider` has been dropped. +- The third argument of custom `storesToProps` functions passed to `inject` is no longer available. +- `` no longer supports the deprecated `inject` property. +- Defining `shouldComponentUpdate` on `observer` based components is no longer supported +- `propTypes` is no longer exposed, use `PropTypes` instead +- `disposeOnUnmount` now only supports direct subclasses of `React.Component` and `React.PureComponent`. This prevents several unreliable edge cases that silently leaked memory before. Either only extend React.(Pure)Component when using `disposeOnUnmount`, or manually clean up stuff in `componentWillUnmount`. +- The `onError` global error handler has been removed. Use error boundaries instead. +- Improved dev tool names for `inject` wrapped components, see [#472](https://github.com/mobxjs/mobx-react/pull/472) by [SimeonC](https://github.com/SimeonC). Fixes [#466](https://github.com/mobxjs/mobx-react/issues/466) +- Dropped support for a build of mobx-react that doesn't target either `react-dom` or `react-native`. mobx-react doesn't need `react-dom` to be present, but to make sure your build tools don't fail, you might want to stub `react-dom` as an empty module. +- The `componentWillReact` has been dropped +- The MobX-react devtools (either as package or browser plugin) are no longer supported. Instead, the following tools can be analyzed to analyze your mobx-react application: + - Visualizing re-rendering of components is now part of the standard React devtools + - The dependency tree of a compent tree can be inspected by showing the state of the `useObserver` hook in the React devtools (at the time of this release it displays as just `Object`, but the next iteration of the React devtools will support those properly) + - Spying on events can still be done with the [MobX-react browser plugin](https://github.com/mobxjs/mobx-devtools), through the [mobx-logger](https://github.com/winterbe/mobx-logger) package or manually by using the `spy` or `trace` utility from the mobx package. + +**Improvements** + +- Hook based components are now supported by mobx-react (in fact, the package is now implemented using hooks) +- Class based `observer` components are now _recommended_ to extend `React.PureComponent`. Functional `observer` components are now automatically wrapped in `React.memo` internally. See section in [README](https://mobx.js.org/README.html#observercomponentclass) for more details. +- For `observer` based components, there will now be an additional `Observer` component in the tree. +- Two new hooks have been exposed, in case you want to manage local state in observable: `useLocalStore` and `useAsObservableSource`. +- `MobXProviderContext` is now exposed from the package, in case you want to consume the context used by `Provider` with a `useContext` hook. + +## 5.4.3 + +- Fixed [#612](https://github.com/mobxjs/mobx-react/issues/612), `contextType` was hoisted by `inject`, which shouldn't the case. + +## 5.4.1 / 5.4.2 + +- Fixed issue where `react-is` wasn't properly rolled-up into the package. Fixes [#608](https://github.com/mobxjs/mobx-react/issues/608) + +## 5.4.0 + +- Added support for forward refs, fixes [#602](https://github.com/mobxjs/mobx-react/issues/602) + +## 5.3.6 + +- Fixed some additional issues around life-cycle patching, take 3. See [#536](https://github.com/mobxjs/mobx-react/pull/586) by [@xaviergonz](https://github.com/xaviergonz). Fixed [#579](https://github.com/mobxjs/mobx-react/issues/579) + +## 5.3.5 + +- Fixed some additional issues around life-cycle patching, see [#583](https://github.com/mobxjs/mobx-react/pull/583) by [@xaviergonz](https://github.com/xaviergonz). Fixed [#581](https://github.com/mobxjs/mobx-react/issues/581) + +## 5.3.4 + +- Fixed unending recursing as a result of lifecylce patching. Fixes [#579](https://github.com/mobxjs/mobx-react/issues/579) through [#582](https://github.com/mobxjs/mobx-react/pull/582) by [@xaviergonz](https://github.com/xaviergonz) + +## 5.3.3 + +- Fixed `Cannot read property 'forEach' of undefined` exception if `disposeOnUnmount` was called conditionally. [#578](https://github.com/mobxjs/mobx-react/pull/578) by [Jef Hellemans](https://github.com/JefHellemans) + +## 5.3.2 + +- Fixed: "process not defined", [#574](https://github.com/mobxjs/mobx-react/pull/574/) through [#576](https://github.com/mobxjs/mobx-react/pull/576/) by [@xaviergonz](https://github.com/xaviergonz) + +## 5.3.0 / 5.3.1 + +_5.3.0 was retracted as files were not generated correctly during publish_ + +- Added `disposeOnUnmount` utility / decorator to call disposable properties (reaction, autorun, etc) automatically on `componentWillUnmount` +- Introduced new method to patch lifecycle methods which should be more compatible with for example arrow functions. + +## 5.2.8 + +- Make sure `mobx-react` doesn't require `Object.assign` polyfill + +## 5.2.7 + +- Fixed issue where React 16.5 printed a warning when using `Provider`, fixes [#545](https://github.com/mobxjs/mobx-react/issues/545) + +## 5.2.6 + +- Fixed bug in defining properties (although the bug had no known observable effect). Fixes [#540](https://github.com/mobxjs/mobx-react/issues/540) + +## 5.2.4 / 5.2.5 + +- Improved compatibility with React-Hot-Loader, see [#522](https://github.com/mobxjs/mobx-react/pull/522) by [theKashey](https://github.com/theKashey). Fixes [#500](https://github.com/mobxjs/mobx-react/issues/500) + +## 5.2.3 + +- Fixed problem with `Symbol` feature detection. By [@Strate](https://github.com/Strate) through [#501](https://github.com/mobxjs/mobx-react/pull/501). Fixes [#498](https://github.com/mobxjs/mobx-react/issues/498) and [#503](https://github.com/mobxjs/mobx-react/issues/503). + +## 5.2.2 + +- Polyfill `Symbol` if it doesn't exist. By [@Strate](https://github.com/Strate) through [#499](https://github.com/mobxjs/mobx-react/pull/499). + +## 5.2.1 + +- Component `props` and `state` properties are now made observable during the instance creation. This restores the behavior from before 5.1.0 where `props` and `state` could safely be observed during mount. Actually it is now possible to do similar things in constructors as well. Fixes [#478](https://github.com/mobxjs/mobx-react/issues/478). Thanks [@Strate](https://github.com/Strate) for the idea and PR! [#496](https://github.com/mobxjs/mobx-react/pull/496). + +## 5.2.0 + +- Added backward compatible support for MobX 5. +- Fixed components sometimes being displayed as `undefined` in mobx-devtools. See [#470](https://github.com/mobxjs/mobx-react/pull/470) by [@MauricioAndrades](https://github.com/MauricioAndrades) +- Removed unnecessary warning `@observer` was used both on a sub and super class. See [#492](https://github.com/mobxjs/mobx-react/pull/476) by [@skiritsis](https://github.com/skiritsis). _N.B. putting `@observer` on a super and subclass is still not an supported pattern, use @observer on subclasses only!_ + +## 5.1.2 + +- Fixed regression bug in integration with devtools. Fixed through [#465](https://github.com/mobxjs/mobx-react/pull/465) by @le0nik + +## 5.1.0 + +- Added support for React 16.3, including support for the `getDerivedStateFromProps` life-cycle hook. MobX will no longer use `componentWillMount` hook internally, so that it can be used in `StrictMode` react as well. Fixes [#447](https://github.com/mobxjs/mobx-react/issues/447) +- Static properties of a function component are now automatically hoisted when the component is wrapped by `observer`. Implements [#427](https://github.com/mobxjs/mobx-react/pull/427) +- Misspelled export `componentByNodeRegistery` is now properly export as `componentByNodeRegistry` as well, please update consumers, the mispelled version will be dropped in the next major. Fixes [#421](https://github.com/mobxjs/mobx-react/issues/421) +- Deprecated the support for the `inject` property on `Observer`, it is fundamentally broken and should not be used. Use `inject` on the enclosing component instead and grab the necessary stores from the closure. Fixes [#423](https://github.com/mobxjs/mobx-react/issues/423) +- Added warning about using `observer` on a React.PureComponent, this will become an exception in the next major. Fixes [#309](https://github.com/mobxjs/mobx-react/issues/309) +- Mobx-react will now print a warning when combining `observer` with a custom `shouldComponentUpdate` implementation. Fixes [#417](https://github.com/mobxjs/mobx-react/issues/417) + +## 5.0.0 + +- Added compatibility with MobX 4.x. This version is not compatible with older Mobx versions + +## 4.4.3 + +- The exposed React Native build now uses commonjs, to prevent the need of further transpilation. Fixes [#428](https://github.com/mobxjs/mobx-react/issues/428) + +## 4.4.2 + +- Fixed issue with mobx-react not compiling on react-native due to the presence of a `.babelrc` file. Fixes [#415](https://github.com/mobxjs/mobx-react/issues/415) by [Ryan Rampersad](https://github.com/ryanmr) through [#416](https://github.com/mobxjs/mobx-react/pull/416) + +## 4.4.1 + +- Fixed syntax error in 4.4.0 that escaped + +## 4.4.0 + +- `Observer` now supports render props, `render` and `inject`. See the updated readme. By [ZiYingMai](https://github.com/Sunshine168) through [#403](https://github.com/mobxjs/mobx-react/pull/403) +- Fixed: `NaN` is now considered to be equal to `NaN` when doing reconciliation. Fixes [#363](https://github.com/mobxjs/mobx-react/issues/363), by [Andrew Branch](https://github.com/andrewbranch) through [#402](https://github.com/mobxjs/mobx-react/pull/402) +- Improved typings of `Observer` component, by [RafaƂ Filipek](https://github.com/RafalFilipek) through [#376](https://github.com/mobxjs/mobx-react/pull/376) +- Fixed incorrect generation of component name, by [Andy Kogut](https://github.com/andykog) through [#368](https://github.com/mobxjs/mobx-react/pull/368) +- Lot of internal repo upgrades: Test suite is now in Jest, Prettier is used etc. + +## 4.3.5 + +Fixed some issues with the typescript typings. See for example #353 + +## 4.3.4 + +Improved typescript typings, including support for `strict` mode in TS 2.6. Fixes + +## 4.3.3 + +Added support for React 16. (No changes) + +## 4.3.2 + +Killed accidentally exposed default exports. + +If you are still using `import mobxReact from "mobx-react"`, use `import * as mobxReact from "mobx-react"`, or better `import { whatYouNeed } from "mobx-react"` instead. + +## 4.3.1 + +## 4.3.0 (unpublished) + +Improved module rollup setup, enabling better tree shaking. See #324 / #328 + +## 4.2.2 + +- Fixed check for stateless components, by @leader22, see #280 + +## 4.2.1 + +_Note: Due to pull / rebase issue the release commit is incorrect. This is the released [commit](https://github.com/mobxjs/mobx-react/commit/f1b3eefc5239cb451b317204fa8aad94b4dcfc2f)_ + +- Reduced module size by 31% (switched to rollup.js). See #244 by @rossipedia +- Skip creation of `.wrappedInstance` reference for stateless components. See #254 by @farwayer +- Introduced global `onError` handler hook to be notified on errors thrown by `@observer` components. See #262 by @andykog +- Improved typescript typings of the exposed `propTypes`, See #263 by @panjiesw + +## 4.2.0 + +- Same as 4.2.1, but contained build issue and is unpublished + +## 4.1.8 + +- Undid change introduced in 4.1.4 where the lifecycle hooks were protected, as this breaks react-hot-loader.... Fixes #231 + +## 4.1.7 + +- Added support for React 15.5 (no deprecation warnings) and 16.0 (no proptypes / createClass), by @andykog, see #238. Fixes #233, #237 + +## 4.1.5 + +- Improved typescript typings, fixes #223 + +## 4.1.4 + +- Made lifecycle hooks used by mobx-react read-only to make sure they are not accidentally overwritten in component instances. Fixes, #195, #202. Note that they can still be defined, just make sure to define them on the prototype (`componentWillMount() {}`) instead of the instance (`componentWillMount = () => {}`). Which is best practice anyway. + +## 4.1.3 + +- Fixed `ReactDOM.findDOMNode` exception when using react-test-runner, #216 + +## 4.1.2 + +- Exceptions caught during render are now rethrown with proper stack, fixes #206 + +## 4.1.1 + +- Exposed `wrappedInstance` and `wrappedComponent` in typings +- Fixed accidental use of `default` import from `mobx` package. + +## 4.1.0 + +- Added support for MobX3. Note that using MobX3 changes the error semantics. If an `observer` component throws, it will no longer crash the app, but just log the exceptions instead. + +## 4.0.4 + +- Introduced `suppressChangedStoreWarning` to optionally supresss change store warnings, by @dropfen, see #182, #183 + +## 4.0.3 + +- Fixed issue where userland componentWilMount was run before observer componentWillMount + +## 4.0.2 + +- Fixed order of `inject` overloads, see #169 +- Fixed import of `mobx` when using Webpack without commonjs plugin, see: #168 + +## 4.0.1 + +- Improved typings, by @timmolendijk, fixes #164, #166 +- Fixed `inject` signature in readme, by @farwayer + +## 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 }) =>

{name}

+ +const UserNameDisplayer = inject(stores => ({ + name: stores.userStore.name +}))(NameDisplayer) + +const user = mobx.observable({ + name: "Noa" +}) + +const App = () => ( + + + +) + +ReactDOM.render(, 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

{this.upperCaseName}

+ } +} +``` + +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 }) => {() =>
{user.name}
}
+``` + +### 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 + +- Print warning when `inject` and `observer` are used in the wrong order, see #146, by @delaetthomas + +## 3.5.8 + +- Fixed issue where `props` where not passed properly to components in very rare cases. Also fixed #115 + +## 3.5.7 + +- Bundles are no longer minified, fixes #127 + +## 3.5.6 + +- Export `propTypes` as `PropTypes`, like React (@andykog, ##117) + +## 3.5.5 + +- Removed `experimental` status of `inject` / `Provider`. Official feature now. +- Fixed hot-reloading issue, #101 + +## 3.5.4 + +- Introduced `wrappedInstance` by @rossipedia on `inject` decorated HOC's, see https://github.com/mobxjs/mobx-react/pull/90/ +- print warnings when assign values to `propTypes`, `defaultProps`, or `contextTypes` of a HOC. (by @jtraub, see https://github.com/mobxjs/mobx-react/pull/88/) +- Static properties are now hoisted to HoC components when, #92 +- If `inject` is used incombination with a function, the object return from the function will now be merged into the `nextProps` instead of replacing them, #80 +- Always do propType checking untracked, partially fixes #56, #305 + +## 3.5.3 + +- Fixed error `Cannot read property 'renderReporter' of undefined` (#96) + +## 3.5.2 + +- Added propTypes.observableArrayOf and propTypes.arrayOrObservableArrayOf (#91) + +## 3.5.1 + +- Fixed regression #85, changes caused by the constructor results in inconsistent rendering (N.B.: that is un-idiomatic React usage and React will warn about this!) + +## 3.5.0 + +- Introduced `inject("store1", "store2")(component)` as alternative syntax to inject stores. Should address #77, #70 +- Introduced the `wrappedComponent` property on injected higher order components, addresses #70, #72 +- Fixed #76: error when no stores are provided through context +- Added typings for devTools related features (@benjamingr). +- Added MobX specific propTypes (@mattruby) +- Merged #44, fixes #73: don't re-render if component was somehow unmounted + +## 3.4.0 + +- Introduced `Provider` / context support (#53 / MobX #300) +- Fixed issues when using devtools with IE. #66 (By @pvasek) + +## 3.3.1 + +- Added typescript typings form `mobx-react/native` and `mobx-react/custom` +- Fixed #63: error when using stateless function components when using babel and typescript + +## 3.3.0 + +- Upgraded to MobX 2.2.0 + +## 3.2.0 + +- Added support for react-native 0.25 and higher. By @danieldunderfelt. + +## 3.1.0 + +- Added support for custom renderers (without DOM), use: `mobx-react/custom` as import fixes #42 +- Fixed some issues with rollup #43 +- Minor optimization + +## 3.0.5 + +Introduced `componentWillReact` + +## 3.0.4 + +The debug name stateless function components of babel transpiled jsx are now properly picked up if the wrapper is applied after defining the component: + +```javascript +const MyComponent = () => hi + +export default observer(MyComponent) +``` + +## 3.0.3 + +Removed peer dependencies, React 15 (and 0.13) are supported as well. By @bkniffler + +## 3.0.2 + +Removed the warning introduced in 3.0.1. It triggered always when using shallow rendering (when using shallow rendering `componentDidMount` won't fire. See https://github.com/facebook/react/issues/4919). + +## 3.0.1 + +Added warning when changing state in `getInitialState` / `constructor`. + +## 3.0.0 + +Upgraded to MobX 2.0.0 + +## 2.1.5 + +Improved typescript typings overloads of `observer` + +## 2.1.4 + +Added empty 'dependencies' section to package.json, fixes #26 + +## 2.1.3 + +Added support for context to stateless components. (by Kosta-Github). + +## 2.1.1 + +Fixed #12: fixed React warning when a component was unmounted after scheduling a re-render but before executing it. + +## 2.1.0 + +Upped dependency of mobx to 1.1.1. + +## 2.0.1 + +It is now possible to define `propTypes` and `getDefaultProps` on a stateless component: + +```javascript +const myComponent = props => { + // render +} + +myComponent.propTypes = { + name: React.PropTypes.string +} + +myComponent.defaultProps = { + name: "World" +} + +export default observer(myComponent) +``` + +All credits to Jiri Spac for this contribution! + +## 2.0.0 + +Use React 0.14 instead of React 0.13. For React 0.13, use version `mobx-react@1.0.2` or higher. + +## 1.0.2 + +Minor fixes and improvements + +## 1.0.1 + +Fixed issue with typescript typings. An example project with MobX, React, Typescript, TSX can be found here: https://github.com/mobxjs/mobx-react-typescript + +## 1.0.0 + +`reactiveComponent` has been renamed to `observer` + +### 0.2.3 + +Added separte import for react-native: use `var reactiveComponent = require('mobx-react/native').reactiveComponent` for native support; webpack clients will refuse to build otherwise. + +### 0.2.2 + +Added react-native as dependency, so that the package works with either `react` or `react-native`. + +### 0.2.0 + +Upgraded to MobX 0.7.0 + +### 0.1.7 + +Fixed issue where Babel generated component classes where not properly picked up. + +### 0.1.6 + +`observer` now accepts a pure render function as argument, besides constructor function. For example: + +```javascript +var TodoItem = observer(function TodoItem(props) { + var todo = props.todo + return
  • {todo.task}
  • +}) +``` + +### 0.1.5 + +observer is now defined in terms of side effects. + +### 0.1.4 + +Added support for React 0.14(RC) by dropping peer dependency diff --git a/packages/mobx-react/LICENSE b/packages/mobx-react/LICENSE new file mode 100644 index 0000000000..b58becae8c --- /dev/null +++ b/packages/mobx-react/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Michel Weststrate + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/mobx-react/README.md b/packages/mobx-react/README.md new file mode 100644 index 0000000000..7ee8e5c482 --- /dev/null +++ b/packages/mobx-react/README.md @@ -0,0 +1,506 @@ +# mobx-react + +[![CircleCI](https://circleci.com/gh/mobxjs/mobx-react.svg?style=svg)](https://circleci.com/gh/mobxjs/mobx-react) +[![CDNJS](https://img.shields.io/cdnjs/v/mobx-react.svg)](https://cdnjs.com/libraries/mobx-react) +[![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) + +[![Discuss on Github](https://img.shields.io/badge/discuss%20on-GitHub-orange)](https://github.com/mobxjs/mobx/discussions) +[![View changelog](https://img.shields.io/badge/changelogs.xyz-Explore%20Changelog-brightgreen)](https://changelogs.xyz/mobx-react) + +Package with React component wrapper for combining React with MobX. +Exports the `observer` decorator and other utilities. +For documentation, see the [MobX](https://mobxjs.github.io/mobx) project. +There is also work-in-progress [user guide](https://mobx-react.js.org) for additional information. +This package supports both React and React Native. + +## Choosing your version + +There are currently two actively maintained versions of mobx-react: + +| NPM Version | Support MobX version | Supported React versions | Supports hook based components | +| ----------- | -------------------- | ------------------------ | -------------------------------------------------------------------------------- | +| v7 | 6.\* | 16.8+ | Yes | +| v6 | 4._ / 5._ | 16.8+ | Yes | +| v5 | 4._ / 5._ | 0.13+ | No, but it is possible to use `` sections inside hook based components | + +mobx-react 6 / 7 is a repackage of the smaller [mobx-react-lite](https://github.com/mobxjs/mobx-react-lite) package + following features from the `mobx-react@5` package added: + +- Support for class based components for `observer` and `@observer` +- `Provider / inject` to pass stores around (but consider to use `React.createContext` instead) +- `PropTypes` to describe observable based property checkers (but consider to use TypeScript instead) +- The `disposeOnUnmount` utility / decorator to easily clean up resources such as reactions created in your class based components. + +## Installation + +`npm install mobx-react --save` + +Or CDN: https://unpkg.com/mobx-react (UMD namespace: `mobxReact`) + +```javascript +import { observer } from "mobx-react" +``` + +This package provides the bindings for MobX and React. +See the [official documentation](https://mobx.js.org/react-integration.html) for how to get started. + +For greenfield projects you might want to consider to use [mobx-react-lite](https://github.com/mobxjs/mobx-react-lite), if you intend to only use function based components. `React.createContext` can be used to pass stores around. + +## API documentation + +Please check [mobx.js.org](https://mobx.js.org/) for the general documentation. The documentation below highlights some specifics. + +### `observer(component)` + +Function (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. + +#### Functional Components + +`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. + +#### Class Components + +When using component classes, `this.props` and `this.state` will be made observables, so the component will react to all changes in props and state that are used by `render`. + +`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. + +See the [MobX](https://mobxjs.github.io/mobx/refguide/observer-component.html) documentation for more details. + +```javascript +import { observer } from "mobx-react" + +// ---- ES6 syntax ---- +const TodoView = observer( + class TodoView extends React.Component { + render() { + return
    {this.props.todo.title}
    + } + } +) + +// ---- ESNext syntax with decorator syntax enabled ---- +@observer +class TodoView extends React.Component { + render() { + return
    {this.props.todo.title}
    + } +} + +// ---- or just use function components: ---- +const TodoView = observer(({ todo }) =>
    {todo.title}
    ) +``` + +### `Observer` + +`Observer` is a React component, which applies `observer` to an anonymous 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 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. + +```javascript +class App extends React.Component { + render() { + return ( +
    + {this.props.person.name} + {() =>
    {this.props.person.name}
    }
    +
    + ) + } +} + +const person = observable({ name: "John" }) + +ReactDOM.render(, document.body) +person.name = "Mike" // will cause the Observer region to re-render +``` + +In 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. +Example + +```javascript +class App extends React.Component { + render() { + return ( +
    + {this.props.person.name} +
    {this.props.person.name}
    } /> +
    + ) + } +} + +const person = observable({ name: "John" }) + +ReactDOM.render(, document.body) +person.name = "Mike" // will cause the Observer region to re-render +``` + +### `useLocalObservable` hook + +[User guide](https://mobx-react.js.org/state-local) + +Local observable state can be introduced by using the `useLocalObservable` hook, that runs once to create an observable store. A quick example would be: + +```javascript +import { useLocalObservable, Observer } from "mobx-react-lite" + +const Todo = () => { + const todo = useLocalObservable(() => ({ + title: "Test", + done: true, + toggle() { + this.done = !this.done + } + })) + + return ( + + {() => ( +

    + {todo.title} {todo.done ? "[DONE]" : "[TODO]"} +

    + )} +
    + ) +} +``` + +When 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. + +It 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. + +Instead, if your store needs to refer to props (or `useState` based local state), the `useLocalObservable` should be combined with the `useAsObservableSource` hook, see below. + +Note 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. + +_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._ + +### Server Side Rendering with `enableStaticRendering` + +When using server side rendering, normal lifecycle hooks of React components are not fired, as the components are rendered only once. +Since components are never unmounted, `observer` components would in this case leak memory when being rendered server side. +To avoid leaking memory, call `enableStaticRendering(true)` when using server side rendering. + +```javascript +import { enableStaticRendering } from "mobx-react" + +enableStaticRendering(true) +``` + +This makes sure the component won't try to react to any future data changes. + +### Which components should be marked with `observer`? + +The simple rule of thumb is: _all components that render observable data_. +If 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. + +### Enabling decorators (optional) + +Decorators are currently a stage-2 ESNext feature. How to enable them is documented [here](https://github.com/mobxjs/mobx#enabling-decorators-optional). + +### Should I still use smart and dumb components? + +See this [thread](https://www.reddit.com/r/reactjs/comments/4vnxg5/free_eggheadio_course_learn_mobx_react_in_30/d61oh0l). +TL;DR: the conceptual distinction makes a lot of sense when using MobX as well, but use `observer` on all components. + +### `PropTypes` + +MobX-react provides the following additional `PropTypes` which can be used to validate against MobX structures: + +- `observableArray` +- `observableArrayOf(React.PropTypes.number)` +- `observableMap` +- `observableObject` +- `arrayOrObservableArray` +- `arrayOrObservableArrayOf(React.PropTypes.number)` +- `objectOrObservableObject` + +Use `import { PropTypes } from "mobx-react"` to import them, then use for example `PropTypes.observableArray` + +### `Provider` and `inject` + +See also [the migration guide to React Hooks](https://mobx-react.js.org/recipes-migration). + +_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`._ + +`Provider` is a component that can pass stores (or other stuff) using React's context mechanism to child components. +This is useful if you have things that you don't want to pass through multiple layers of components explicitly. + +`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. + +Example (based on the official [context docs](https://facebook.github.io/react/docs/context.html#passing-info-automatically-through-a-tree)): + +```javascript +@inject("color") +@observer +class Button extends React.Component { + render() { + return + } +} + +class Message extends React.Component { + render() { + return ( +
    + {this.props.text} +
    + ) + } +} + +class MessageList extends React.Component { + render() { + const children = this.props.messages.map(message => ) + return ( + +
    {children}
    +
    + ) + } +} +``` + +Notes: + +- 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`. +- 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! +- 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. +- The original component wrapped by `inject` is available as the `wrappedComponent` property of the created higher order component. + +#### "The set of provided stores has changed" error + +Values 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. +This 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://mobx-react.js.org/recipes-context) directly which does not have this restriction. + +#### Inject as function + +The above example in ES5 would start like: + +```javascript +var Button = inject("color")( + observer( + class Button extends Component { + /* ... etc ... */ + } + ) +) +``` + +A functional stateless component would look like: + +```javascript +var Button = inject("color")( + observer(({ color }) => { + /* ... etc ... */ + }) +) +``` + +#### 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 +const NameDisplayer = ({ name }) =>

    {name}

    + +const UserNameDisplayer = inject(stores => ({ + name: stores.userStore.name +}))(NameDisplayer) + +const user = mobx.observable({ + name: "Noa" +}) + +const App = () => ( + + + +) + +ReactDOM.render(, document.body) +``` + +_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_ + +#### 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 + +##### With TypeScript + +`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. +The `additionalProps` will be merged into the original `nextProps` before being provided to the next component. + +```typescript +import { IUserStore } from "myStore" + +@inject(allStores => ({ + userStore: allStores.userStore as IUserStore +})) +class MyComponent extends React.Component<{ userStore?: IUserStore; otherProp: number }, {}> { + /* etc */ +} +``` + +Make sure to mark `userStore` as an optional property. It should not (necessarily) be passed in by parent components at all! + +Note: If you have strict null checking enabled, you could muffle the nullable type by using the `!` operator: + +``` +public render() { + const {a, b} = this.store! + // ... +} +``` + +By [migrating to React Hooks](https://mobx-react.js.org/recipes-migration) you can avoid problems with TypeScript. + +#### Testing store injection + +It 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. + +So if you have in your app something like: + +```javascript + + + +``` + +In your test you can easily test the `Person` component by passing the necessary store as prop directly: + +``` +const profile = new Profile() +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()` + +### disposeOnUnmount(componentInstance, propertyKey | function | function[]) + +Function (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. + +```javascript +import { disposeOnUnmount } from "mobx-react" + +class SomeComponent extends React.Component { + // decorator version + @disposeOnUnmount + someReactionDisposer = reaction(...) + + // decorator version with arrays + @disposeOnUnmount + someReactionDisposers = [ + reaction(...), + reaction(...) + ] + + + // function version over properties + someReactionDisposer = disposeOnUnmount(this, reaction(...)) + + // function version inside methods + componentDidMount() { + // single function + disposeOnUnmount(this, reaction(...)) + + // or function array + disposeOnUnmount(this, [ + reaction(...), + reaction(...) + ]) + } +} +``` + +## DevTools + +`mobx-react@6` and higher are no longer compatible with the mobx-react-devtools. +That is, the MobX react devtools will no longer show render timings or dependency trees of the component. +The reason is that the standard React devtools are also capable of highlighting re-rendering components. +And the dependency tree of a component can now be inspected by the standard devtools as well, as shown in the image below: + +![hooks.png](hooks.png) + +## FAQ + +**Should I use `observer` for each component?** + +You should use `observer` on every component that displays observable data. +Even the small ones. `observer` allows components to render independently from their parent and in general this means that +the more you use `observer`, the better the performance become. +The overhead of `observer` itself is negligible. +See also [Do child components need `@observer`?](https://github.com/mobxjs/mobx/issues/101) + +**I see React warnings about `forceUpdate` / `setState` from React** + +The following warning will appear if you trigger a re-rendering between instantiating and rendering a component: + +``` + +Warning: forceUpdate(...): Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.` + +``` + +-- or -- + +``` + +Warning: 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`. + +``` + +Usually this means that (another) component is trying to modify observables used by this components in their `constructor` or `getInitialState` methods. +This violates the React Lifecycle, `componentWillMount` should be used instead if state needs to be modified before mounting. diff --git a/packages/mobx-react/__mocks__/react-native.js b/packages/mobx-react/__mocks__/react-native.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/mobx-react/__tests__/.eslintrc.yaml b/packages/mobx-react/__tests__/.eslintrc.yaml new file mode 100644 index 0000000000..111b92a95a --- /dev/null +++ b/packages/mobx-react/__tests__/.eslintrc.yaml @@ -0,0 +1,5 @@ +env: + jest: true +rules: + "react/display-name": "off" + "react/prop-types": "off" diff --git a/packages/mobx-react/__tests__/Provider.test.tsx b/packages/mobx-react/__tests__/Provider.test.tsx new file mode 100644 index 0000000000..220118fbf9 --- /dev/null +++ b/packages/mobx-react/__tests__/Provider.test.tsx @@ -0,0 +1,85 @@ +import React from "react" +import { Provider } from "../src" +import { render } from "@testing-library/react" +import { MobXProviderContext } from "../src/Provider" +import { withConsole } from "./utils/withConsole" + +describe("Provider", () => { + it("should work in a simple case", () => { + function A() { + return ( + + {({ foo }) => foo} + + ) + } + + const { container } = render() + expect(container).toHaveTextContent("bar") + }) + + it("should not provide the children prop", () => { + function A() { + return ( + + + {stores => + Reflect.has(stores, "children") + ? "children was provided" + : "children was not provided" + } + + + ) + } + + const { container } = render() + expect(container).toHaveTextContent("children was not provided") + }) + + it("supports overriding stores", () => { + function B() { + return ( + + {({ overridable, nonOverridable }) => `${overridable} ${nonOverridable}`} + + ) + } + + function A() { + return ( + + + + + + + ) + } + const { container } = render() + expect(container).toMatchInlineSnapshot(` +
    + original original + overriden original +
    +`) + }) + + it("should throw an error when changing stores", () => { + function A({ foo }) { + return ( + + {({ foo }) => foo} + + ) + } + + const { rerender } = render(
    ) + + withConsole(() => { + expect(() => { + rerender() + }).toThrow("The set of provided stores has changed.") + }) + }) +}) diff --git a/packages/mobx-react/__tests__/__snapshots__/observer.test.tsx.snap b/packages/mobx-react/__tests__/__snapshots__/observer.test.tsx.snap new file mode 100644 index 0000000000..af4107f6bb --- /dev/null +++ b/packages/mobx-react/__tests__/__snapshots__/observer.test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`issue 12 1`] = ` +
    +
    + + coffee + ! + + + tea + + +
    +
    +`; + +exports[`issue 12 2`] = ` +
    +
    + + soup + + +
    +
    +`; + +exports[`should stop updating if error was thrown in render (#134) 1`] = ` +Array [ + "Error: Hello", + Object { + "componentStack": " + in X + in div (created by Outer) + in Outer", + }, +] +`; diff --git a/packages/mobx-react/__tests__/__snapshots__/stateless.test.tsx.snap b/packages/mobx-react/__tests__/__snapshots__/stateless.test.tsx.snap new file mode 100644 index 0000000000..77eca06af3 --- /dev/null +++ b/packages/mobx-react/__tests__/__snapshots__/stateless.test.tsx.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`stateless component with forwardRef is reactive 1`] = ` +
    +
    + result: + hello world + , + got ref + , a.x: + 2 +
    +
    +`; + +exports[`stateless component with forwardRef render test correct 1`] = ` +
    +
    + result: + hello world + , + got ref + , a.x: + 1 +
    +
    +`; diff --git a/packages/mobx-react/__tests__/context.test.tsx b/packages/mobx-react/__tests__/context.test.tsx new file mode 100644 index 0000000000..5a0ae7fb2e --- /dev/null +++ b/packages/mobx-react/__tests__/context.test.tsx @@ -0,0 +1,134 @@ +import React from "react" +import { observable } from "mobx" +import { Provider, observer, inject } from "../src" +import { withConsole } from "./utils/withConsole" +import { render, act } from "@testing-library/react" +import { any } from "prop-types" + +test("no warnings in modern react", () => { + const box = observable.box(3) + const Child = inject("store")( + observer( + class Child extends React.Component { + render() { + return ( +
    + {this.props.store} + {box.get()} +
    + ) + } + } + ) + ) + + class App extends React.Component { + render() { + return ( +
    + + + + + +
    + ) + } + } + + const { container } = render() + expect(container).toHaveTextContent("42 + 3") + + withConsole(["info", "warn", "error"], () => { + act(() => { + box.set(4) + }) + expect(container).toHaveTextContent("42 + 4") + + expect(console.info).not.toHaveBeenCalled() + expect(console.warn).not.toHaveBeenCalled() + expect(console.error).not.toHaveBeenCalled() + }) +}) + +test("getDerivedStateFromProps works #447", () => { + class Main extends React.Component { + static getDerivedStateFromProps(nextProps, prevState) { + return { + count: prevState.count + 1 + } + } + + state = { + count: 0 + } + + render() { + return ( +
    +

    {`${this.state.count ? "One " : "No "}${this.props.thing}`}

    +
    + ) + } + } + + const MainInjected = inject(({ store }) => ({ thing: store.thing }))(Main) + + const store = { thing: 3 } + + const App = () => ( + + + + ) + + const { container } = render() + expect(container).toHaveTextContent("One 3") +}) + +test("no double runs for getDerivedStateFromProps", () => { + let derived = 0 + @observer + class Main extends React.Component { + state = { + activePropertyElementMap: {} + } + + constructor(props) { + // console.log("CONSTRUCTOR") + super(props) + } + + static getDerivedStateFromProps() { + derived++ + // console.log("PREVSTATE", nextProps) + return null + } + + render() { + return
    Test-content
    + } + } + // This results in + //PREVSTATE + //CONSTRUCTOR + //PREVSTATE + let MainInjected = inject(() => ({ + componentProp: "def" + }))(Main) + // Uncomment the following line to see default behaviour (without inject) + //CONSTRUCTOR + //PREVSTATE + //MainInjected = Main; + + const store = {} + + const App = () => ( + + + + ) + + const { container } = render() + expect(container).toHaveTextContent("Test-content") + expect(derived).toBe(1) +}) diff --git a/packages/mobx-react/__tests__/disposeOnUnmount.test.tsx b/packages/mobx-react/__tests__/disposeOnUnmount.test.tsx new file mode 100644 index 0000000000..fb398b4612 --- /dev/null +++ b/packages/mobx-react/__tests__/disposeOnUnmount.test.tsx @@ -0,0 +1,489 @@ +import React from "react" +import { disposeOnUnmount, observer } from "../src" +import { render } from "@testing-library/react" +import { MockedComponentClass } from "react-dom/test-utils" + +interface ClassC extends MockedComponentClass { + methodA?: any + methodB?: any + methodC?: any + methodD?: any +} + +function testComponent(C: ClassC, afterMount?: Function, afterUnmount?: Function) { + const ref = React.createRef() + const { unmount } = render() + + let cref = ref.current + expect(cref?.methodA).not.toHaveBeenCalled() + expect(cref?.methodB).not.toHaveBeenCalled() + if (afterMount) { + afterMount(cref) + } + + unmount() + + expect(cref?.methodA).toHaveBeenCalledTimes(1) + expect(cref?.methodB).toHaveBeenCalledTimes(1) + if (afterUnmount) { + afterUnmount(cref) + } +} + +describe("without observer", () => { + test("class without componentWillUnmount", async () => { + class C extends React.Component { + @disposeOnUnmount + methodA = jest.fn() + @disposeOnUnmount + methodB = jest.fn() + @disposeOnUnmount + methodC = null + @disposeOnUnmount + methodD = undefined + + render() { + return null + } + } + + testComponent(C) + }) + + test("class with componentWillUnmount in the prototype", () => { + let called = 0 + + class C extends React.Component { + @disposeOnUnmount + methodA = jest.fn() + @disposeOnUnmount + methodB = jest.fn() + @disposeOnUnmount + methodC = null + @disposeOnUnmount + methodD = undefined + + render() { + return null + } + + componentWillUnmount() { + called++ + } + } + + testComponent( + C, + () => { + expect(called).toBe(0) + }, + () => { + expect(called).toBe(1) + } + ) + }) + + test.skip("class with componentWillUnmount as an arrow function", () => { + let called = 0 + + class C extends React.Component { + @disposeOnUnmount + methodA = jest.fn() + @disposeOnUnmount + methodB = jest.fn() + @disposeOnUnmount + methodC = null + @disposeOnUnmount + methodD = undefined + + render() { + return null + } + + componentWillUnmount = () => { + called++ + } + } + + testComponent( + C, + () => { + expect(called).toBe(0) + }, + () => { + expect(called).toBe(1) + } + ) + }) + + test("class without componentWillUnmount using non decorator version", () => { + let methodC = jest.fn() + let methodD = jest.fn() + class C extends React.Component { + render() { + return null + } + + methodA = disposeOnUnmount(this, jest.fn()) + methodB = disposeOnUnmount(this, jest.fn()) + + constructor(props) { + super(props) + disposeOnUnmount(this, [methodC, methodD]) + } + } + + testComponent( + C, + () => { + expect(methodC).not.toHaveBeenCalled() + expect(methodD).not.toHaveBeenCalled() + }, + () => { + expect(methodC).toHaveBeenCalledTimes(1) + expect(methodD).toHaveBeenCalledTimes(1) + } + ) + }) +}) + +describe("with observer", () => { + test("class without componentWillUnmount", () => { + @observer + class C extends React.Component { + @disposeOnUnmount + methodA = jest.fn() + @disposeOnUnmount + methodB = jest.fn() + @disposeOnUnmount + methodC = null + @disposeOnUnmount + methodD = undefined + + render() { + return null + } + } + + testComponent(C) + }) + + test("class with componentWillUnmount in the prototype", () => { + let called = 0 + + @observer + class C extends React.Component { + @disposeOnUnmount + methodA = jest.fn() + @disposeOnUnmount + methodB = jest.fn() + @disposeOnUnmount + methodC = null + @disposeOnUnmount + methodD = undefined + + render() { + return null + } + + componentWillUnmount() { + called++ + } + } + + testComponent( + C, + () => { + expect(called).toBe(0) + }, + () => { + expect(called).toBe(1) + } + ) + }) + + test.skip("class with componentWillUnmount as an arrow function", () => { + let called = 0 + + @observer + class C extends React.Component { + @disposeOnUnmount + methodA = jest.fn() + @disposeOnUnmount + methodB = jest.fn() + @disposeOnUnmount + methodC = null + @disposeOnUnmount + methodD = undefined + + render() { + return null + } + + componentWillUnmount = () => { + called++ + } + } + + testComponent( + C, + () => { + expect(called).toBe(0) + }, + () => { + expect(called).toBe(1) + } + ) + }) + + test("class without componentWillUnmount using non decorator version", () => { + let methodC = jest.fn() + let methodD = jest.fn() + + @observer + class C extends React.Component { + render() { + return null + } + + methodA = disposeOnUnmount(this, jest.fn()) + methodB = disposeOnUnmount(this, jest.fn()) + + constructor(props) { + super(props) + disposeOnUnmount(this, [methodC, methodD]) + } + } + + testComponent( + C, + () => { + expect(methodC).not.toHaveBeenCalled() + expect(methodD).not.toHaveBeenCalled() + }, + () => { + expect(methodC).toHaveBeenCalledTimes(1) + expect(methodD).toHaveBeenCalledTimes(1) + } + ) + }) +}) + +it("componentDidMount should be different between components", () => { + function doTest(withObserver) { + const events: Array = [] + + class A extends React.Component { + didMount + willUnmount + + componentDidMount() { + this.didMount = "A" + events.push("mountA") + } + + componentWillUnmount() { + this.willUnmount = "A" + events.push("unmountA") + } + + render() { + return null + } + } + + class B extends React.Component { + didMount + willUnmount + + componentDidMount() { + this.didMount = "B" + events.push("mountB") + } + + componentWillUnmount() { + this.willUnmount = "B" + events.push("unmountB") + } + + render() { + return null + } + } + + if (withObserver) { + // @ts-ignore + // eslint-disable-next-line no-class-assign + A = observer(A) + // @ts-ignore + // eslint-disable-next-line no-class-assign + B = observer(B) + } + + const aRef = React.createRef
    () + const { rerender, unmount } = render() + const caRef = aRef.current + + expect(caRef?.didMount).toBe("A") + expect(caRef?.willUnmount).toBeUndefined() + expect(events).toEqual(["mountA"]) + + const bRef = React.createRef() + rerender() + const cbRef = bRef.current + + expect(caRef?.didMount).toBe("A") + expect(caRef?.willUnmount).toBe("A") + + expect(cbRef?.didMount).toBe("B") + expect(cbRef?.willUnmount).toBeUndefined() + expect(events).toEqual(["mountA", "unmountA", "mountB"]) + + unmount() + + expect(caRef?.didMount).toBe("A") + expect(caRef?.willUnmount).toBe("A") + + expect(cbRef?.didMount).toBe("B") + expect(cbRef?.willUnmount).toBe("B") + expect(events).toEqual(["mountA", "unmountA", "mountB", "unmountB"]) + } + + doTest(true) + doTest(false) +}) + +test("base cWU should not be called if overriden", () => { + let baseCalled = 0 + let dCalled = 0 + let oCalled = 0 + + class C extends React.Component { + componentWillUnmount() { + baseCalled++ + } + + constructor(props) { + super(props) + this.componentWillUnmount = () => { + oCalled++ + } + } + + render() { + return null + } + + @disposeOnUnmount + fn() { + dCalled++ + } + } + const { unmount } = render() + unmount() + expect(dCalled).toBe(1) + expect(oCalled).toBe(1) + expect(baseCalled).toBe(0) +}) + +test("should error on inheritance", () => { + class C extends React.Component { + render() { + return null + } + } + + expect(() => { + // eslint-disable-next-line no-unused-vars + class B extends C { + @disposeOnUnmount + fn() {} + } + }).toThrow("disposeOnUnmount only supports direct subclasses") +}) + +test("should error on inheritance - 2", () => { + class C extends React.Component { + render() { + return null + } + } + + class B extends C { + fn + constructor(props) { + super(props) + expect(() => { + this.fn = disposeOnUnmount(this, function () {}) + }).toThrow("disposeOnUnmount only supports direct subclasses") + } + } + + render() +}) + +describe("should work with arrays", () => { + test("as a function", () => { + class C extends React.Component { + methodA = jest.fn() + methodB = jest.fn() + + componentDidMount() { + disposeOnUnmount(this, [this.methodA, this.methodB]) + } + + render() { + return null + } + } + + testComponent(C) + }) + + test("as a decorator", () => { + class C extends React.Component { + methodA = jest.fn() + methodB = jest.fn() + + @disposeOnUnmount + disposers = [this.methodA, this.methodB] + + render() { + return null + } + } + + testComponent(C) + }) +}) + +it("runDisposersOnUnmount only runs disposers from the declaring instance", () => { + class A extends React.Component { + @disposeOnUnmount + a = jest.fn() + + b = jest.fn() + + constructor(props) { + super(props) + disposeOnUnmount(this, this.b) + } + + render() { + return null + } + } + + const ref1 = React.createRef() + const ref2 = React.createRef() + const { unmount } = render() + render() + const inst1 = ref1.current + const inst2 = ref2.current + unmount() + + expect(inst1?.a).toHaveBeenCalledTimes(1) + expect(inst1?.b).toHaveBeenCalledTimes(1) + expect(inst2?.a).toHaveBeenCalledTimes(0) + expect(inst2?.b).toHaveBeenCalledTimes(0) +}) diff --git a/packages/mobx-react/__tests__/hooks.test.tsx b/packages/mobx-react/__tests__/hooks.test.tsx new file mode 100644 index 0000000000..97fea060e3 --- /dev/null +++ b/packages/mobx-react/__tests__/hooks.test.tsx @@ -0,0 +1,90 @@ +import React from "react" +import { observer, Observer, useLocalStore, useAsObservableSource } from "../src" +import { render, act } from "@testing-library/react" + +afterEach(() => { + jest.useRealTimers() +}) + +test("computed properties react to props when using hooks", async () => { + jest.useFakeTimers() + + const seen: Array = [] + + const Child = ({ x }) => { + const props = useAsObservableSource({ x }) + const store = useLocalStore(() => ({ + get getPropX() { + return props.x + } + })) + + return ( + {() => (seen.push(store.getPropX), (
    {store.getPropX}
    ))}
    + ) + } + + const Parent = () => { + const [state, setState] = React.useState({ x: 0 }) + seen.push("parent") + React.useEffect(() => { + setTimeout(() => { + act(() => { + setState({ x: 2 }) + }) + }) + }, []) + return + } + + const { container } = render() + expect(container).toHaveTextContent("0") + + jest.runAllTimers() + expect(seen).toEqual(["parent", 0, "parent", 2]) + expect(container).toHaveTextContent("2") +}) + +test("computed properties result in double render when using observer instead of Observer", async () => { + jest.useFakeTimers() + + const seen: Array = [] + + const Child = observer(({ x }) => { + const props = useAsObservableSource({ x }) + const store = useLocalStore(() => ({ + get getPropX() { + return props.x + } + })) + + seen.push(store.getPropX) + return
    {store.getPropX}
    + }) + + const Parent = () => { + const [state, setState] = React.useState({ x: 0 }) + seen.push("parent") + React.useEffect(() => { + setTimeout(() => { + act(() => { + setState({ x: 2 }) + }) + }, 100) + }, []) + return + } + + const { container } = render() + expect(container).toHaveTextContent("0") + + jest.runAllTimers() + expect(seen).toEqual([ + "parent", + 0, + "parent", + 2, + 2 // should contain "2" only once! But with hooks, one update is scheduled based the fact that props change, the other because the observable source changed. + ]) + expect(container).toHaveTextContent("2") +}) diff --git a/packages/mobx-react/__tests__/inject.test.tsx b/packages/mobx-react/__tests__/inject.test.tsx new file mode 100644 index 0000000000..9a48c9d57f --- /dev/null +++ b/packages/mobx-react/__tests__/inject.test.tsx @@ -0,0 +1,531 @@ +import React from "react" +import PropTypes from "prop-types" +import { action, observable, makeObservable } from "mobx" +import { observer, inject, Provider } from "../src" +import { IValueMap } from "../src/types/IValueMap" +import { render, act } from "@testing-library/react" +import { withConsole } from "./utils/withConsole" +import { IReactComponent } from "../src/types/IReactComponent" + +describe("inject based context", () => { + test("basic context", () => { + const C = inject("foo")( + observer( + class X extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + ) + const B = () => + const A = () => ( + + + + ) + const { container } = render(
    ) + expect(container).toHaveTextContent("context:bar") + }) + + test("props override context", () => { + const C = inject("foo")( + class T extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + const B = () => + const A = class T extends React.Component { + render() { + return ( + + + + ) + } + } + const { container } = render(
    ) + expect(container).toHaveTextContent("context:42") + }) + + test("wraps displayName of original component", () => { + const A: React.ComponentClass = inject("foo")( + class ComponentA extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + const B: React.ComponentClass = inject()( + class ComponentB extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + const C: React.ComponentClass = inject(() => ({}))( + class ComponentC extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + expect(A.displayName).toBe("inject-with-foo(ComponentA)") + expect(B.displayName).toBe("inject(ComponentB)") + expect(C.displayName).toBe("inject(ComponentC)") + }) + + // FIXME: see other comments related to error catching in React + // test does work as expected when running manually + test("store should be available", () => { + const C = inject("foo")( + observer( + class C extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + ) + const B = () => + const A = class A extends React.Component { + render() { + return ( + + + + ) + } + } + + withConsole(() => { + expect(() => render(
    )).toThrow( + /Store 'foo' is not available! Make sure it is provided by some Provider/ + ) + }) + }) + + test("store is not required if prop is available", () => { + const C = inject("foo")( + observer( + class C extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + ) + const B = () => + const { container } = render() + expect(container).toHaveTextContent("context:bar") + }) + + test("inject merges (and overrides) props", () => { + const C = inject(() => ({ a: 1 }))( + observer( + class C extends React.Component { + render() { + expect(this.props).toEqual({ a: 1, b: 2 }) + return null + } + } + ) + ) + const B = () => + render() + }) + + test("custom storesToProps", () => { + const C = inject((stores: IValueMap, props: any) => { + expect(stores).toEqual({ foo: "bar" }) + expect(props).toEqual({ baz: 42 }) + return { + zoom: stores.foo, + baz: props.baz * 2 + } + })( + observer( + class C extends React.Component { + render() { + return ( +
    + context: + {this.props.zoom} + {this.props.baz} +
    + ) + } + } + ) + ) + const B = class B extends React.Component { + render() { + return + } + } + const A = () => ( + + + + ) + const { container } = render(
    ) + expect(container).toHaveTextContent("context:bar84") + }) + + test("inject forwards ref", () => { + class FancyComp extends React.Component { + didRender + render() { + this.didRender = true + return null + } + + doSomething() {} + } + + const ref = React.createRef() + render() + expect(typeof ref.current?.doSomething).toBe("function") + expect(ref.current?.didRender).toBe(true) + + const InjectedFancyComp = inject("bla")(FancyComp) + const ref2 = React.createRef() + + render( + + + + ) + + expect(typeof ref2.current?.doSomething).toBe("function") + expect(ref2.current?.didRender).toBe(true) + }) + + test("inject should work with components that use forwardRef", () => { + const FancyComp = React.forwardRef((_: any, ref: React.Ref) => { + return
    + }) + + const InjectedFancyComp = inject("bla")(FancyComp) + const ref = React.createRef() + + render( + + + + ) + + expect(ref.current).not.toBeNull() + expect(ref.current).toBeInstanceOf(HTMLDivElement) + }) + + test("support static hoisting, wrappedComponent and ref forwarding", () => { + class B extends React.Component { + static foo + static bar + testField + + render() { + this.testField = 1 + return null + } + } + ;(B as React.ComponentClass).propTypes = { + x: PropTypes.object + } + B.foo = 17 + B.bar = {} + const C = inject("booh")(B) + expect(C.wrappedComponent).toBe(B) + expect(B.foo).toBe(17) + expect(C.foo).toBe(17) + expect(C.bar === B.bar).toBeTruthy() + expect(Object.keys(C.wrappedComponent.propTypes!)).toEqual(["x"]) + + const ref = React.createRef() + + render() + expect(ref.current?.testField).toBe(1) + }) + + test("propTypes and defaultProps are forwarded", () => { + const msg: Array = [] + const baseError = console.error + console.error = m => msg.push(m) + + const C: React.ComponentClass = inject("foo")( + class C extends React.Component { + render() { + expect(this.props.y).toEqual(3) + + expect(this.props.x).toBeUndefined() + return null + } + } + ) + C.propTypes = { + x: PropTypes.func.isRequired, + z: PropTypes.string.isRequired + } + // @ts-ignore + C.wrappedComponent.propTypes = { + a: PropTypes.func.isRequired + } + C.defaultProps = { + y: 3 + } + const B = () => + const A = () => ( + + + + ) + render() + expect(msg.length).toBe(2) + expect(msg[0].split("\n")[0]).toBe( + "Warning: Failed prop type: The prop `x` is marked as required in `inject-with-foo(C)`, but its value is `undefined`." + ) + expect(msg[1].split("\n")[0]).toBe( + "Warning: Failed prop type: The prop `a` is marked as required in `C`, but its value is `undefined`." + ) + console.error = baseError + }) + + test("warning is not printed when attaching propTypes to injected component", () => { + let msg = [] + const baseWarn = console.warn + console.warn = m => (msg = m) + + const C: React.ComponentClass = inject("foo")( + class C extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + C.propTypes = {} + + expect(msg.length).toBe(0) + console.warn = baseWarn + }) + + test("warning is not printed when attaching propTypes to wrappedComponent", () => { + let msg = [] + const baseWarn = console.warn + console.warn = m => (msg = m) + const C = inject("foo")( + class C extends React.Component { + render() { + return ( +
    + context: + {this.props.foo} +
    + ) + } + } + ) + C.wrappedComponent.propTypes = {} + expect(msg.length).toBe(0) + console.warn = baseWarn + }) + + test("using a custom injector is reactive", () => { + const user = observable({ name: "Noa" }) + const mapper = stores => ({ name: stores.user.name }) + const DisplayName = props =>

    {props.name}

    + const User = inject(mapper)(DisplayName) + const App = () => ( + + + + ) + const { container } = render() + expect(container).toHaveTextContent("Noa") + + act(() => { + user.name = "Veria" + }) + expect(container).toHaveTextContent("Veria") + }) + + test("using a custom injector is not too reactive", () => { + let listRender = 0 + let itemRender = 0 + let injectRender = 0 + + function connect() { + const args = arguments + return (component: IReactComponent) => + // @ts-ignore + inject.apply(this, args)(observer(component)) + } + + class State { + @observable + highlighted = null + isHighlighted(item) { + return this.highlighted == item + } + + @action + highlight = item => { + this.highlighted = item + } + + constructor() { + makeObservable(this) + } + } + + const items = observable([ + { title: "ItemA" }, + { title: "ItemB" }, + { title: "ItemC" }, + { title: "ItemD" }, + { title: "ItemE" }, + { title: "ItemF" } + ]) + + const state = new State() + + class ListComponent extends React.PureComponent { + render() { + listRender++ + const { items } = this.props + + return ( +
      + {items.map(item => ( + + ))} +
    + ) + } + } + + // @ts-ignore + @connect(({ state }, { item }) => { + injectRender++ + if (injectRender > 6) { + // debugger; + } + return { + // Using + // highlighted: expr(() => state.isHighlighted(item)) // seems to fix the problem + highlighted: state.isHighlighted(item), + highlight: state.highlight + } + }) + class ItemComponent extends React.PureComponent { + highlight = () => { + const { item, highlight } = this.props + highlight(item) + } + + render() { + itemRender++ + const { highlighted, item } = this.props + return ( +
  • + {item.title} {highlighted ? "(highlighted)" : ""}{" "} +
  • + ) + } + } + + const { container } = render( + + + + ) + + expect(listRender).toBe(1) + expect(injectRender).toBe(6) + expect(itemRender).toBe(6) + + container.querySelectorAll(".hl_ItemB").forEach((e: Element) => (e as HTMLElement).click()) + expect(listRender).toBe(1) + expect(injectRender).toBe(12) // ideally, 7 + expect(itemRender).toBe(7) + + container.querySelectorAll(".hl_ItemF").forEach((e: Element) => (e as HTMLElement).click()) + expect(listRender).toBe(1) + expect(injectRender).toBe(18) // ideally, 9 + expect(itemRender).toBe(9) + }) +}) + +test("#612 - mixed context types", () => { + const SomeContext = React.createContext(true) + + class MainCompClass extends React.Component { + static contextType = SomeContext + render() { + let active = this.context + return active ? this.props.value : "Inactive" + } + } + + const MainComp = inject((stores: any) => ({ + value: stores.appState.value + }))(MainCompClass) + + const appState = observable({ + value: "Something" + }) + + function App() { + return ( + + + + + + ) + } + + render() +}) diff --git a/packages/mobx-react/__tests__/issue21.test.tsx b/packages/mobx-react/__tests__/issue21.test.tsx new file mode 100644 index 0000000000..ff110c79ff --- /dev/null +++ b/packages/mobx-react/__tests__/issue21.test.tsx @@ -0,0 +1,447 @@ +import React, { createElement } from "react" +import { + computed, + isObservable, + observable, + reaction, + transaction, + IReactionDisposer, + makeObservable +} from "mobx" +import { observer } from "../src" +import _ from "lodash" +import { render } from "@testing-library/react" + +let topRenderCount = 0 + +const wizardModel = observable( + { + steps: [ + { + title: "Size", + active: true + }, + { + title: "Fabric", + active: false + }, + { + title: "Finish", + active: false + } + ], + get activeStep() { + return _.find(this.steps, "active") + }, + activateNextStep: function () { + const nextStep = this.steps[_.findIndex(this.steps, "active") + 1] + if (!nextStep) { + return false + } + this.setActiveStep(nextStep) + return true + }, + setActiveStep(modeToActivate) { + const self = this + transaction(() => { + _.find(self.steps, "active").active = false + modeToActivate.active = true + }) + } + } as any, + { + activateNextStep: observable.ref + } +) + +/** RENDERS **/ + +const Wizard = observer( + class Wizard extends React.Component { + render() { + return createElement( + "div", + null, +
    +

    Active Step:

    + +
    , +
    +

    All Step:

    +

    + Clicking on these steps will render the active step just once. This is what + I expected. +

    + +
    + ) + } + } +) + +const WizardStep = observer( + class WizardStep extends React.Component { + renderCount = 0 + componentWillUnmount() { + // console.log("Unmounting!") + } + render() { + // weird test hack: + if (this.props.tester === true) { + topRenderCount++ + } + return createElement( + "div", + { onClick: this.modeClickHandler }, + "RenderCount: " + + this.renderCount++ + + " " + + this.props.step.title + + ": isActive:" + + this.props.step.active + ) + } + modeClickHandler = () => { + var step = this.props.step + wizardModel.setActiveStep(step) + } + } +) + +/** END RENDERERS **/ + +const changeStep = stepNumber => wizardModel.setActiveStep(wizardModel.steps[stepNumber]) + +test("verify issue 21", () => { + render() + expect(topRenderCount).toBe(1) + changeStep(0) + expect(topRenderCount).toBe(2) + changeStep(2) + expect(topRenderCount).toBe(3) +}) + +test("verify prop changes are picked up", () => { + function createItem(subid, label) { + const res = observable( + { + subid, + id: 1, + label: label, + get text() { + events.push(["compute", this.subid]) + return ( + this.id + + "." + + this.subid + + "." + + this.label + + "." + + data.items.indexOf(this as any) + ) + } + }, + {}, + { proxy: false } + ) + res.subid = subid // non reactive + return res + } + const data = observable({ + items: [createItem(1, "hi")] + }) + const events: Array = [] + const Child = observer( + class Child extends React.Component { + componentDidUpdate(prevProps) { + events.push(["update", prevProps.item.subid, this.props.item.subid]) + } + render() { + events.push(["render", this.props.item.subid, this.props.item.text]) + return {this.props.item.text} + } + } + ) + + const Parent = observer( + class Parent extends React.Component { + render() { + return ( +
    + {data.items.map(item => ( + + ))} +
    + ) + } + } + ) + + const Wrapper = () => + + function changeStuff() { + transaction(() => { + data.items[0].label = "hello" // schedules state change for Child + data.items[0] = createItem(2, "test") // Child should still receive new prop! + }) + + // @ts-ignore + this.setState({}) // trigger update + } + + const { container } = render() + expect(events.sort()).toEqual( + [ + ["compute", 1], + ["render", 1, "1.1.hi.0"] + ].sort() + ) + events.splice(0) + let testDiv = container.querySelector("#testDiv")! as HTMLElement + testDiv.click() + expect(events.sort()).toEqual( + [ + ["compute", 1], + ["update", 1, 2], + ["compute", 2], + ["render", 2, "1.2.test.0"] + ].sort() + ) +}) + +test("verify props is reactive", () => { + function createItem(subid, label) { + const res = observable( + { + subid, + id: 1, + label: label, + get text() { + events.push(["compute", this.subid]) + return ( + this.id + + "." + + this.subid + + "." + + this.label + + "." + + data.items.indexOf(this as any) + ) + } + }, + {}, + { proxy: false } + ) + res.subid = subid // non reactive + return res + } + + const data = observable({ + items: [createItem(1, "hi")] + }) + const events: Array = [] + + class Child extends React.Component { + constructor(p) { + super(p) + makeObservable(this) + } + + @computed + get computedLabel() { + events.push(["computed label", this.props.item.subid]) + return this.props.item.label + } + componentDidMount() { + events.push(["mount"]) + } + componentDidUpdate(prevProps) { + events.push(["update", prevProps.item.subid, this.props.item.subid]) + } + render() { + events.push(["render", this.props.item.subid, this.props.item.text, this.computedLabel]) + return ( + + {this.props.item.text} + {this.computedLabel} + + ) + } + } + + const ChildAsObserver = observer(Child) + + const Parent = observer( + class Parent extends React.Component { + render() { + return ( +
    + {data.items.map(item => ( + + ))} +
    + ) + } + } + ) + + const Wrapper = () => + + function changeStuff() { + transaction(() => { + // components start rendeirng a new item, but computed is still based on old value + data.items = [createItem(2, "test")] + }) + } + + const { container } = render() + expect(events.sort()).toEqual( + [["mount"], ["compute", 1], ["computed label", 1], ["render", 1, "1.1.hi.0", "hi"]].sort() + ) + + events.splice(0) + let testDiv = container.querySelector("#testDiv") as HTMLElement + testDiv.click() + + expect(events.sort()).toEqual( + [ + ["compute", 1], + ["update", 1, 2], + ["compute", 2], + ["computed label", 2], + ["render", 2, "1.2.test.0", "test"] + ].sort() + ) +}) + +test("no re-render for shallow equal props", async () => { + function createItem(subid, label) { + const res = observable({ + subid, + id: 1, + label: label + }) + res.subid = subid // non reactive + return res + } + + const data = observable({ + items: [createItem(1, "hi")], + parentValue: 0 + }) + const events: Array> = [] + + const Child = observer( + class Child extends React.Component { + componentDidMount() { + events.push(["mount"]) + } + componentDidUpdate(prevProps) { + events.push(["update", prevProps.item.subid, this.props.item.subid]) + } + render() { + events.push(["render", this.props.item.subid, this.props.item.label]) + return {this.props.item.label} + } + } + ) + + const Parent = observer( + class Parent extends React.Component { + render() { + // "object has become observable!" + expect(isObservable(this.props.nonObservable)).toBeFalsy() + events.push(["parent render", data.parentValue]) + return ( +
    + {data.items.map(item => ( + + ))} +
    + ) + } + } + ) + + const Wrapper = () => + + function changeStuff() { + data.items[0].label = "hi" // no change. + data.parentValue = 1 // rerender parent + } + + const { container } = render() + expect(events.sort()).toEqual([["parent render", 0], ["mount"], ["render", 1, "hi"]].sort()) + events.splice(0) + let testDiv = container.querySelector("#testDiv") as HTMLElement + testDiv.click() + expect(events.sort()).toEqual([["parent render", 1]].sort()) +}) + +test("lifecycle callbacks called with correct arguments", () => { + var Comp = observer( + class Comp extends React.Component { + componentDidUpdate(prevProps) { + expect(prevProps.counter).toBe(0) + expect(this.props.counter).toBe(1) + } + render() { + return ( +
    + {[this.props.counter]} +
    + ) + } + } + ) + const Root = class T extends React.Component { + state = { counter: 0 } + onButtonClick = () => { + this.setState({ counter: (this.state.counter || 0) + 1 }) + } + render() { + return + } + } + const { container } = render() + let testButton = container.querySelector("#testButton") as HTMLElement + testButton.click() +}) + +test("verify props are reactive in constructor", () => { + const propValues: Array = [] + let constructorCallsCount = 0 + + const Component = observer( + class Component extends React.Component { + disposer: IReactionDisposer + constructor(props, context) { + super(props, context) + constructorCallsCount++ + this.disposer = reaction( + () => this.props.prop, + prop => propValues.push(prop), + { + fireImmediately: true + } + ) + } + + componentWillUnmount() { + this.disposer() + } + + render() { + return
    + } + } + ) + + const { rerender } = render() + rerender() + rerender() + rerender() + expect(constructorCallsCount).toEqual(1) + expect(propValues).toEqual(["1", "2", "3", "4"]) +}) diff --git a/packages/mobx-react/__tests__/issue806.test.tsx b/packages/mobx-react/__tests__/issue806.test.tsx new file mode 100644 index 0000000000..56158d7725 --- /dev/null +++ b/packages/mobx-react/__tests__/issue806.test.tsx @@ -0,0 +1,52 @@ +import React from "react" +import { configure, observable } from "mobx" +import { observer } from "../src" +import { render } from "@testing-library/react" +import { withConsole } from "./utils/withConsole" + +@observer +class Issue806Component extends React.Component { + render() { + return ( + + {this.props.a} + + + ) + } +} + +@observer +class Issue806Component2 extends React.Component { + render() { + return ( + + {this.props.propA} - {this.props.propB} + + ) + } +} + +test("verify issue 806", () => { + configure({ + observableRequiresReaction: true + }) + + const x = observable({ + a: 1 + }) + + withConsole(["warn"], () => { + render() + expect(console.warn).not.toHaveBeenCalled() + }) + + // make sure observableRequiresReaction is still working outside component + withConsole(["warn"], () => { + x.a.toString() + expect(console.warn).toBeCalledTimes(1) + expect(console.warn).toHaveBeenCalledWith( + "[mobx] Observable ObservableObject@1.a being read outside a reactive context" + ) + }) +}) diff --git a/packages/mobx-react/__tests__/misc.test.tsx b/packages/mobx-react/__tests__/misc.test.tsx new file mode 100644 index 0000000000..eaac20a7c3 --- /dev/null +++ b/packages/mobx-react/__tests__/misc.test.tsx @@ -0,0 +1,87 @@ +import React from "react" +import { extendObservable, observable } from "mobx" +import { observer } from "../src" +import { render } from "@testing-library/react" + +test("issue mobx 405", () => { + function ExampleState() { + // @ts-ignore + extendObservable(this, { + name: "test", + get greetings() { + return "Hello my name is " + this.name + } + }) + } + + const ExampleView = observer( + class T extends React.Component { + render() { + return ( +
    + (this.props.exampleState.name = e.target.value)} + value={this.props.exampleState.name} + /> + {this.props.exampleState.greetings} +
    + ) + } + } + ) + + const exampleState = new ExampleState() + const { container } = render() + expect(container).toMatchInlineSnapshot(` +
    +
    + + + Hello my name is test + +
    +
    +`) +}) + +test("#85 Should handle state changing in constructors", () => { + const a = observable.box(2) + const Child = observer( + class Child extends React.Component { + constructor(p) { + super(p) + a.set(3) // one shouldn't do this! + this.state = {} + } + render() { + return ( +
    + child: + {a.get()} -{" "} +
    + ) + } + } + ) + const ParentWrapper = observer(function Parent() { + return ( + + + parent: + {a.get()} + + ) + }) + const { container } = render() + expect(container).toHaveTextContent("child:3 - parent:3") + + a.set(5) + expect(container).toHaveTextContent("child:5 - parent:5") + + a.set(7) + expect(container).toHaveTextContent("child:7 - parent:7") +}) diff --git a/packages/mobx-react/__tests__/observer.test.tsx b/packages/mobx-react/__tests__/observer.test.tsx new file mode 100644 index 0000000000..29ac296c08 --- /dev/null +++ b/packages/mobx-react/__tests__/observer.test.tsx @@ -0,0 +1,883 @@ +import React from "react" +import { inject, observer, Observer, enableStaticRendering } from "../src" +import { render, act } from "@testing-library/react" +import { + getObserverTree, + _resetGlobalState, + action, + computed, + observable, + transaction, + makeObservable +} from "mobx" +import { withConsole } from "./utils/withConsole" +/** + * some test suite is too tedious + */ + +afterEach(() => { + jest.useRealTimers() +}) + +/* + use TestUtils.renderIntoDocument will re-mounted the component with different props + some misunderstanding will be cause +*/ +describe("nestedRendering", () => { + let store + + let todoItemRenderings + const TodoItem = observer(function TodoItem(props) { + todoItemRenderings++ + return
  • |{props.todo.title}
  • + }) + + let todoListRenderings + const TodoList = observer( + class TodoList extends React.Component { + render() { + todoListRenderings++ + const todos = store.todos + return ( +
    + {todos.length} + {todos.map((todo, idx) => ( + + ))} +
    + ) + } + } + ) + + beforeEach(() => { + todoItemRenderings = 0 + todoListRenderings = 0 + store = observable({ + todos: [ + { + title: "a", + completed: false + } + ] + }) + }) + + test("first rendering", () => { + const { container } = render() + + expect(todoListRenderings).toBe(1) + expect(container.querySelectorAll("li").length).toBe(1) + expect(container.querySelector("li")).toHaveTextContent("|a") + expect(todoItemRenderings).toBe(1) + }) + + test("second rendering with inner store changed", () => { + render() + + store.todos[0].title += "a" + + expect(todoListRenderings).toBe(1) + expect(todoItemRenderings).toBe(2) + expect(getObserverTree(store, "todos").observers!.length).toBe(1) + expect(getObserverTree(store.todos[0], "title").observers!.length).toBe(1) + }) + + test("rerendering with outer store added", () => { + const { container } = render() + + store.todos.push({ + title: "b", + completed: true + }) + + expect(container.querySelectorAll("li").length).toBe(2) + expect( + Array.from(container.querySelectorAll("li")) + .map((e: any) => e.innerHTML) + .sort() + ).toEqual(["|a", "|b"].sort()) + expect(todoListRenderings).toBe(2) + expect(todoItemRenderings).toBe(2) + expect(getObserverTree(store.todos[1], "title").observers!.length).toBe(1) + expect(getObserverTree(store.todos[1], "completed").observers).toBe(undefined) + }) + + test("rerendering with outer store pop", () => { + const { container } = render() + + const oldTodo = store.todos.pop() + + expect(todoListRenderings).toBe(2) + expect(todoItemRenderings).toBe(1) + expect(container.querySelectorAll("li").length).toBe(0) + expect(getObserverTree(oldTodo, "title").observers).toBe(undefined) + expect(getObserverTree(oldTodo, "completed").observers).toBe(undefined) + }) +}) + +describe("isObjectShallowModified detects when React will update the component", () => { + const store = observable({ count: 0 }) + let counterRenderings = 0 + const Counter: React.FunctionComponent = observer(function TodoItem() { + counterRenderings++ + return
    {store.count}
    + }) + + test("does not assume React will update due to NaN prop", () => { + render() + + store.count++ + + expect(counterRenderings).toBe(2) + }) +}) + +describe("keep views alive", () => { + let yCalcCount + let data + const TestComponent = observer(function testComponent() { + return ( +
    + {data.z} + {data.y} +
    + ) + }) + + beforeEach(() => { + yCalcCount = 0 + data = observable({ + x: 3, + get y() { + yCalcCount++ + return this.x * 2 + }, + z: "hi" + }) + }) + + test("init state", () => { + const { container } = render() + + expect(yCalcCount).toBe(1) + expect(container).toHaveTextContent("hi6") + }) + + test("rerender should not need a recomputation of data.y", () => { + const { container } = render() + + data.z = "hello" + + expect(yCalcCount).toBe(1) + expect(container).toHaveTextContent("hello6") + }) +}) + +describe("does not views alive when using static rendering", () => { + let renderCount + let data + + const TestComponent = observer(function testComponent() { + renderCount++ + return
    {data.z}
    + }) + + beforeAll(() => { + enableStaticRendering(true) + }) + + beforeEach(() => { + renderCount = 0 + data = observable({ + z: "hi" + }) + }) + + afterAll(() => { + enableStaticRendering(false) + }) + + test("init state is correct", () => { + const { container } = render() + + expect(renderCount).toBe(1) + expect(container).toHaveTextContent("hi") + }) + + test("no re-rendering on static rendering", () => { + const { container } = render() + + data.z = "hello" + + expect(getObserverTree(data, "z").observers).toBe(undefined) + expect(renderCount).toBe(1) + expect(container).toHaveTextContent("hi") + }) +}) + +test("issue 12", () => { + const events: Array = [] + const data = observable({ + selected: "coffee", + items: [ + { + name: "coffee" + }, + { + name: "tea" + } + ] + }) + + /** Row Class */ + class Row extends React.Component { + constructor(props) { + super(props) + } + + render() { + events.push("row: " + this.props.item.name) + return ( + + {this.props.item.name} + {data.selected === this.props.item.name ? "!" : ""} + + ) + } + } + /** table stateles component */ + const Table = observer(function table() { + events.push("table") + JSON.stringify(data) + return ( +
    + {data.items.map(item => ( + + ))} +
    + ) + }) + + const { container } = render() + expect(container).toMatchSnapshot() + + act(() => { + transaction(() => { + data.items[1].name = "boe" + data.items.splice(0, 2, { name: "soup" }) + data.selected = "tea" + }) + }) + expect(container).toMatchSnapshot() + expect(events).toEqual(["table", "row: coffee", "row: tea", "table", "row: soup"]) +}) + +test("changing state in render should fail", () => { + const data = observable.box(2) + const Comp = observer(() => { + if (data.get() === 3) { + try { + data.set(4) // wouldn't throw first time for lack of observers.. (could we tighten this?) + } catch (err) { + expect( + /Side effects like changing state are not allowed at this point/.test(err) + ).toBeTruthy() + } + } + return
    {data.get()}
    + }) + render() + + data.set(3) + _resetGlobalState() +}) + +test("observer component can be injected", () => { + const msg: Array = [] + const baseWarn = console.warn + console.warn = m => msg.push(m) + + inject("foo")( + observer( + class T extends React.Component { + render() { + return null + } + } + ) + ) + + // N.B, the injected component will be observer since mobx-react 4.0! + inject(() => {})( + observer( + class T extends React.Component { + render() { + return null + } + } + ) + ) + + expect(msg.length).toBe(0) + console.warn = baseWarn +}) + +test("correctly wraps display name of child component", () => { + const A = observer( + class ObserverClass extends React.Component { + render() { + return null + } + } + ) + const B: React.FunctionComponent = observer(function StatelessObserver() { + return null + }) + + expect(A.name).toEqual("ObserverClass") + expect(B.displayName).toEqual("StatelessObserver") +}) + +describe("124 - react to changes in this.props via computed", () => { + class T extends React.Component { + @computed + get computedProp() { + return this.props.x + } + render() { + return ( + + x: + {this.computedProp} + + ) + } + } + + const Comp = observer(T) + + class Parent extends React.Component { + state = { v: 1 } + render() { + return ( +
    this.setState({ v: 2 })}> + +
    + ) + } + } + + test("init state is correct", () => { + const { container } = render() + + expect(container).toHaveTextContent("x:1") + }) + + test("change after click", () => { + const { container } = render() + + container.querySelector("div")!.click() + expect(container).toHaveTextContent("x:2") + }) +}) + +// Test on skip: since all reactions are now run in batched updates, the original issues can no longer be reproduced +//this test case should be deprecated? +test("should stop updating if error was thrown in render (#134)", () => { + const data = observable.box(0) + let renderingsCount = 0 + let lastOwnRenderCount = 0 + const errors: Array = [] + + class Outer extends React.Component { + state = { hasError: false } + + render() { + return this.state.hasError ?
    Error!
    :
    {this.props.children}
    + } + + static getDerivedStateFromError() { + return { hasError: true } + } + + componentDidCatch(error, info) { + errors.push(error.toString().split("\n")[0], info) + } + } + + const Comp = observer( + class X extends React.Component { + ownRenderCount = 0 + + render() { + lastOwnRenderCount = ++this.ownRenderCount + renderingsCount++ + if (data.get() === 2) { + throw new Error("Hello") + } + return
    + } + } + ) + render( + + + + ) + + // Check this + // @ts-ignore + expect(getObserverTree(data).observers!.length).toBe(1) + data.set(1) + expect(renderingsCount).toBe(2) + expect(lastOwnRenderCount).toBe(2) + withConsole(() => { + data.set(2) + }) + + // @ts-ignore + expect(getObserverTree(data).observers).toBe(undefined) + data.set(3) + data.set(4) + data.set(2) + data.set(5) + expect(errors).toMatchSnapshot() + expect(lastOwnRenderCount).toBe(4) + expect(renderingsCount).toBe(4) +}) + +describe("should render component even if setState called with exactly the same props", () => { + let renderCount + const Comp = observer( + class T extends React.Component { + onClick = () => { + this.setState({}) + } + render() { + renderCount++ + return
    + } + } + ) + + beforeEach(() => { + renderCount = 0 + }) + + test("renderCount === 1", () => { + render() + + expect(renderCount).toBe(1) + }) + + test("after click once renderCount === 2", () => { + const { container } = render() + const clickableDiv = container.querySelector("#clickableDiv") as HTMLElement + + clickableDiv.click() + + expect(renderCount).toBe(2) + }) + + test("after click twice renderCount === 3", () => { + const { container } = render() + const clickableDiv = container.querySelector("#clickableDiv") as HTMLElement + + clickableDiv.click() + clickableDiv.click() + + expect(renderCount).toBe(3) + }) +}) + +test("it rerenders correctly if some props are non-observables - 1", () => { + let odata = observable({ x: 1 }) + let data = { y: 1 } + + @observer + class Comp extends React.Component { + @computed + get computed() { + // n.b: data.y would not rerender! shallowly new equal props are not stored + return this.props.odata.x + } + render() { + return ( + + {this.props.odata.x}-{this.props.data.y}-{this.computed} + + ) + } + } + + const Parent = observer( + class Parent extends React.Component { + render() { + // this.props.odata.x; + return + } + } + ) + + function stuff() { + act(() => { + data.y++ + odata.x++ + }) + } + + const { container } = render() + + expect(container).toHaveTextContent("1-1-1") + stuff() + expect(container).toHaveTextContent("2-2-2") + stuff() + expect(container).toHaveTextContent("3-3-3") +}) + +test("it rerenders correctly if some props are non-observables - 2", () => { + let renderCount = 0 + let odata = observable({ x: 1 }) + + @observer + class Component extends React.PureComponent { + @computed + get computed() { + return this.props.data.y // should recompute, since props.data is changed + } + + render() { + renderCount++ + return ( + + {this.props.data.y}-{this.computed} + + ) + } + } + + const Parent = observer(props => { + let data = { y: props.odata.x } + return + }) + + function stuff() { + odata.x++ + } + + const { container } = render() + + expect(renderCount).toBe(1) + expect(container).toHaveTextContent("1-1") + + act(() => stuff()) + expect(renderCount).toBe(2) + expect(container).toHaveTextContent("2-2") + + act(() => stuff()) + expect(renderCount).toBe(3) + expect(container).toHaveTextContent("3-3") +}) + +describe("Observer regions should react", () => { + let data + const Comp = () => ( +
    + {() => {data.get()}} + {data.get()} +
    + ) + + beforeEach(() => { + data = observable.box("hi") + }) + + test("init state is correct", () => { + const { queryByTestId } = render() + + expect(queryByTestId("inside-of-observer")).toHaveTextContent("hi") + expect(queryByTestId("outside-of-observer")).toHaveTextContent("hi") + }) + + test("set the data to hello", () => { + const { queryByTestId } = render() + + data.set("hello") + + expect(queryByTestId("inside-of-observer")).toHaveTextContent("hello") + expect(queryByTestId("outside-of-observer")).toHaveTextContent("hi") + }) +}) + +test("Observer should not re-render on shallow equal new props", () => { + let childRendering = 0 + let parentRendering = 0 + const data = { x: 1 } + const odata = observable({ y: 1 }) + + const Child = observer(({ data }) => { + childRendering++ + return {data.x} + }) + const Parent = observer(() => { + parentRendering++ + odata.y /// depend + return + }) + + const { container } = render() + + expect(parentRendering).toBe(1) + expect(childRendering).toBe(1) + expect(container).toHaveTextContent("1") + + act(() => { + odata.y++ + }) + expect(parentRendering).toBe(2) + expect(childRendering).toBe(1) + expect(container).toHaveTextContent("1") +}) + +test("parent / childs render in the right order", () => { + // See: https://jsfiddle.net/gkaemmer/q1kv7hbL/13/ + let events: Array = [] + + class User { + @observable + name = "User's name" + } + + class Store { + @observable + user: User | null = new User() + @action + logout() { + this.user = null + } + constructor() { + makeObservable(this) + } + } + + function tryLogout() { + try { + // ReactDOM.unstable_batchedUpdates(() => { + store.logout() + expect(true).toBeTruthy() + // }); + } catch (e) { + // t.fail(e) + } + } + + const store = new Store() + + const Parent = observer(() => { + events.push("parent") + if (!store.user) return Not logged in. + return ( +
    + + +
    + ) + }) + + const Child = observer(() => { + events.push("child") + return Logged in as: {store.user?.name} + }) + + render() + + tryLogout() + expect(events).toEqual(["parent", "child", "parent"]) +}) + +describe("use Observer inject and render sugar should work ", () => { + test("use render without inject should be correct", () => { + const Comp = () => ( +
    + {123}} /> +
    + ) + const { container } = render() + expect(container).toHaveTextContent("123") + }) + + test("use children without inject should be correct", () => { + const Comp = () => ( +
    + {() => {123}} +
    + ) + const { container } = render() + expect(container).toHaveTextContent("123") + }) + + test("show error when using children and render at same time ", () => { + const msg: Array = [] + const baseError = console.error + console.error = m => msg.push(m) + + const Comp = () => ( +
    + {123}}>{() => {123}} +
    + ) + + render() + expect(msg.length).toBe(1) + console.error = baseError + }) +}) + +test("use PureComponent", () => { + const msg: Array = [] + const baseWarn = console.warn + console.warn = m => msg.push(m) + + try { + observer( + class X extends React.PureComponent { + return() { + return
    + } + } + ) + + expect(msg).toEqual([]) + } finally { + console.warn = baseWarn + } +}) + +test("static on function components are hoisted", () => { + const Comp = () =>
    + Comp.foo = 3 + + const Comp2 = observer(Comp) + + expect(Comp2.foo).toBe(3) +}) + +test("computed properties react to props", () => { + jest.useFakeTimers() + + const seen: Array = [] + @observer + class Child extends React.Component { + @computed + get getPropX() { + return this.props.x + } + + render() { + seen.push(this.getPropX) + return
    {this.getPropX}
    + } + } + + class Parent extends React.Component { + state = { x: 0 } + render() { + seen.push("parent") + return + } + + componentDidMount() { + setTimeout(() => this.setState({ x: 2 }), 100) + } + } + + const { container } = render() + expect(container).toHaveTextContent("0") + + jest.runAllTimers() + expect(container).toHaveTextContent("2") + + expect(seen).toEqual(["parent", 0, "parent", 2]) +}) + +test("#692 - componentDidUpdate is triggered", () => { + jest.useFakeTimers() + + let cDUCount = 0 + + @observer + class Test extends React.Component { + @observable + counter = 0 + + @action + inc = () => this.counter++ + + constructor(props) { + super(props) + makeObservable(this) + setTimeout(() => this.inc(), 300) + } + + render() { + return

    {this.counter}

    + } + + componentDidUpdate() { + cDUCount++ + } + } + render() + expect(cDUCount).toBe(0) + + jest.runAllTimers() + expect(cDUCount).toBe(1) +}) + +// Not possible to properly test error catching (see ErrorCatcher) +test.skip("#709 - applying observer on React.memo component", () => { + const WithMemo = React.memo(() => { + return null + }) + + const Observed = observer(WithMemo) + // @ts-ignore + // eslint-disable-next-line no-undef + render(, { wrapper: ErrorCatcher }) +}) + +test("#797 - replacing this.render should trigger a warning", () => { + const warn = jest.spyOn(global.console, "warn") + @observer + class Component extends React.Component { + render() { + return
    + } + swapRenderFunc() { + this.render = () => { + return + } + } + } + + const compRef = React.createRef() + const { unmount } = render() + compRef.current?.swapRenderFunc() + unmount() + + expect(warn).toHaveBeenCalled() +}) + +test("Redeclaring an existing observer component as an observer should log a warning", () => { + const warn = jest.spyOn(global.console, "warn") + @observer + class AlreadyObserver extends React.Component { + render() { + return
    + } + } + + observer(AlreadyObserver) + expect(warn).toHaveBeenCalled() +}) diff --git a/packages/mobx-react/__tests__/propTypes.test.ts b/packages/mobx-react/__tests__/propTypes.test.ts new file mode 100644 index 0000000000..6f5a7aa10d --- /dev/null +++ b/packages/mobx-react/__tests__/propTypes.test.ts @@ -0,0 +1,216 @@ +import PropTypes from "prop-types" +import { PropTypes as MRPropTypes } from "../src" +import { observable } from "mobx" + +// Cause `checkPropTypes` caches errors and doesn't print them twice.... +// https://github.com/facebook/prop-types/issues/91 +let testComponentId = 0 + +function typeCheckFail(declaration, value, message) { + const baseError = console.error + let error = "" + console.error = msg => { + error = msg + } + + const props = { testProp: value } + const propTypes = { testProp: declaration } + + const compId = "testComponent" + ++testComponentId + PropTypes.checkPropTypes(propTypes, props, "prop", compId) + + error = error.replace(compId, "testComponent") + expect(error).toBe("Warning: Failed prop type: " + message) + console.error = baseError +} + +function typeCheckFailRequiredValues(declaration) { + const baseError = console.error + let error = "" + console.error = msg => { + error = msg + } + + const propTypes = { testProp: declaration } + const specifiedButIsNullMsg = /but its value is `null`\./ + const unspecifiedMsg = /but its value is `undefined`\./ + + const props1 = { testProp: null } + PropTypes.checkPropTypes(propTypes, props1, "testProp", "testComponent" + ++testComponentId) + expect(specifiedButIsNullMsg.test(error)).toBeTruthy() + + error = "" + const props2 = { testProp: undefined } + PropTypes.checkPropTypes(propTypes, props2, "testProp", "testComponent" + ++testComponentId) + expect(unspecifiedMsg.test(error)).toBeTruthy() + + error = "" + const props3 = {} + PropTypes.checkPropTypes(propTypes, props3, "testProp", "testComponent" + ++testComponentId) + expect(unspecifiedMsg.test(error)).toBeTruthy() + + console.error = baseError +} + +function typeCheckPass(declaration: any, value?: any) { + const props = { testProp: value } + const error = PropTypes.checkPropTypes( + { testProp: declaration }, + props, + "testProp", + "testComponent" + ++testComponentId + ) + expect(error).toBeUndefined() +} + +test("Valid values", () => { + typeCheckPass(MRPropTypes.observableArray, observable([])) + typeCheckPass(MRPropTypes.observableArrayOf(PropTypes.string), observable([""])) + typeCheckPass(MRPropTypes.arrayOrObservableArray, observable([])) + typeCheckPass(MRPropTypes.arrayOrObservableArray, []) + typeCheckPass(MRPropTypes.arrayOrObservableArrayOf(PropTypes.string), observable([""])) + typeCheckPass(MRPropTypes.arrayOrObservableArrayOf(PropTypes.string), [""]) + typeCheckPass(MRPropTypes.observableObject, observable({})) + typeCheckPass(MRPropTypes.objectOrObservableObject, {}) + typeCheckPass(MRPropTypes.objectOrObservableObject, observable({})) + typeCheckPass(MRPropTypes.observableMap, observable(observable.map({}, { deep: false }))) +}) + +test("should be implicitly optional and not warn", () => { + typeCheckPass(MRPropTypes.observableArray) + typeCheckPass(MRPropTypes.observableArrayOf(PropTypes.string)) + typeCheckPass(MRPropTypes.arrayOrObservableArray) + typeCheckPass(MRPropTypes.arrayOrObservableArrayOf(PropTypes.string)) + typeCheckPass(MRPropTypes.observableObject) + typeCheckPass(MRPropTypes.objectOrObservableObject) + typeCheckPass(MRPropTypes.observableMap) +}) + +test("should warn for missing required values, function (test)", () => { + typeCheckFailRequiredValues(MRPropTypes.observableArray.isRequired) + typeCheckFailRequiredValues(MRPropTypes.observableArrayOf(PropTypes.string).isRequired) + typeCheckFailRequiredValues(MRPropTypes.arrayOrObservableArray.isRequired) + typeCheckFailRequiredValues(MRPropTypes.arrayOrObservableArrayOf(PropTypes.string).isRequired) + typeCheckFailRequiredValues(MRPropTypes.observableObject.isRequired) + typeCheckFailRequiredValues(MRPropTypes.objectOrObservableObject.isRequired) + typeCheckFailRequiredValues(MRPropTypes.observableMap.isRequired) +}) + +test("should fail date and regexp correctly", () => { + typeCheckFail( + MRPropTypes.observableObject, + new Date(), + "Invalid prop `testProp` of type `date` supplied to " + + "`testComponent`, expected `mobx.ObservableObject`." + ) + typeCheckFail( + MRPropTypes.observableArray, + /please/, + "Invalid prop `testProp` of type `regexp` supplied to " + + "`testComponent`, expected `mobx.ObservableArray`." + ) +}) + +test("observableArray", () => { + typeCheckFail( + MRPropTypes.observableArray, + [], + "Invalid prop `testProp` of type `array` supplied to " + + "`testComponent`, expected `mobx.ObservableArray`." + ) + typeCheckFail( + MRPropTypes.observableArray, + "", + "Invalid prop `testProp` of type `string` supplied to " + + "`testComponent`, expected `mobx.ObservableArray`." + ) +}) + +test("arrayOrObservableArray", () => { + typeCheckFail( + MRPropTypes.arrayOrObservableArray, + "", + "Invalid prop `testProp` of type `string` supplied to " + + "`testComponent`, expected `mobx.ObservableArray` or javascript `array`." + ) +}) + +test("observableObject", () => { + typeCheckFail( + MRPropTypes.observableObject, + {}, + "Invalid prop `testProp` of type `object` supplied to " + + "`testComponent`, expected `mobx.ObservableObject`." + ) + typeCheckFail( + MRPropTypes.observableObject, + "", + "Invalid prop `testProp` of type `string` supplied to " + + "`testComponent`, expected `mobx.ObservableObject`." + ) +}) + +test("objectOrObservableObject", () => { + typeCheckFail( + MRPropTypes.objectOrObservableObject, + "", + "Invalid prop `testProp` of type `string` supplied to " + + "`testComponent`, expected `mobx.ObservableObject` or javascript `object`." + ) +}) + +test("observableMap", () => { + typeCheckFail( + MRPropTypes.observableMap, + {}, + "Invalid prop `testProp` of type `object` supplied to " + + "`testComponent`, expected `mobx.ObservableMap`." + ) +}) + +test("observableArrayOf", () => { + typeCheckFail( + MRPropTypes.observableArrayOf(PropTypes.string), + 2, + "Invalid prop `testProp` of type `number` supplied to " + + "`testComponent`, expected `mobx.ObservableArray`." + ) + typeCheckFail( + MRPropTypes.observableArrayOf(PropTypes.string), + observable([2]), + "Invalid prop `testProp[0]` of type `number` supplied to " + + "`testComponent`, expected `string`." + ) + typeCheckFail( + MRPropTypes.observableArrayOf({ foo: (MRPropTypes as any).string } as any), + { foo: "bar" }, + "Property `testProp` of component `testComponent` has invalid PropType notation." + ) +}) + +test("arrayOrObservableArrayOf", () => { + typeCheckFail( + MRPropTypes.arrayOrObservableArrayOf(PropTypes.string), + 2, + "Invalid prop `testProp` of type `number` supplied to " + + "`testComponent`, expected `mobx.ObservableArray` or javascript `array`." + ) + typeCheckFail( + MRPropTypes.arrayOrObservableArrayOf(PropTypes.string), + observable([2]), + "Invalid prop `testProp[0]` of type `number` supplied to " + + "`testComponent`, expected `string`." + ) + typeCheckFail( + MRPropTypes.arrayOrObservableArrayOf(PropTypes.string), + [2], + "Invalid prop `testProp[0]` of type `number` supplied to " + + "`testComponent`, expected `string`." + ) + // TODO: + typeCheckFail( + MRPropTypes.arrayOrObservableArrayOf({ foo: (MRPropTypes as any).string } as any), + { foo: "bar" }, + "Property `testProp` of component `testComponent` has invalid PropType notation." + ) +}) diff --git a/packages/mobx-react/__tests__/stateless.test.tsx b/packages/mobx-react/__tests__/stateless.test.tsx new file mode 100644 index 0000000000..9d4ef934da --- /dev/null +++ b/packages/mobx-react/__tests__/stateless.test.tsx @@ -0,0 +1,112 @@ +import React from "react" +import PropTypes from "prop-types" +import { observer, PropTypes as MRPropTypes } from "../src" +import { render, act } from "@testing-library/react" +import { observable } from "mobx" + +const StatelessComp = ({ testProp }) =>
    result: {testProp}
    + +StatelessComp.propTypes = { + testProp: PropTypes.string +} +StatelessComp.defaultProps = { + testProp: "default value for prop testProp" +} + +describe("stateless component with propTypes", () => { + const StatelessCompObserver: React.FunctionComponent = observer(StatelessComp) + + test("default property value should be propagated", () => { + expect(StatelessComp.defaultProps.testProp).toBe("default value for prop testProp") + expect(StatelessCompObserver.defaultProps!.testProp).toBe("default value for prop testProp") + }) + + const originalConsoleError = console.error + let beenWarned = false + console.error = () => (beenWarned = true) + // eslint-disable-next-line no-unused-vars + const wrapper = + console.error = originalConsoleError + + test("an error should be logged with a property type warning", () => { + expect(beenWarned).toBeTruthy() + }) + + test("render test correct", async () => { + const { container } = render() + expect(container.textContent).toBe("result: hello world") + }) +}) + +test("stateless component with context support", () => { + const C = React.createContext({}) + + const StateLessCompWithContext = () => ( + {value =>
    context: {value.testContext}
    }
    + ) + + const StateLessCompWithContextObserver = observer(StateLessCompWithContext) + + const ContextProvider = () => ( + + + + ) + + const { container } = render() + expect(container.textContent).toBe("context: hello world") +}) + +test("component with observable propTypes", () => { + class Comp extends React.Component { + render() { + return null + } + static propTypes = { + a1: MRPropTypes.observableArray, + a2: MRPropTypes.arrayOrObservableArray + } + } + const originalConsoleError = console.error + const warnings: Array = [] + console.error = msg => warnings.push(msg) + // eslint-disable-next-line no-unused-vars + const firstWrapper = + expect(warnings.length).toBe(1) + // eslint-disable-next-line no-unused-vars + const secondWrapper = + expect(warnings.length).toBe(1) + console.error = originalConsoleError +}) + +describe("stateless component with forwardRef", () => { + const a = observable({ + x: 1 + }) + const ForwardRefCompObserver: React.ForwardRefExoticComponent = observer( + React.forwardRef(({ testProp }, ref) => { + return ( +
    + result: {testProp}, {ref ? "got ref" : "no ref"}, a.x: {a.x} +
    + ) + }) + ) + + test("render test correct", () => { + const { container } = render( + + ) + expect(container).toMatchSnapshot() + }) + + test("is reactive", () => { + const { container } = render( + + ) + act(() => { + a.x++ + }) + expect(container).toMatchSnapshot() + }) +}) diff --git a/packages/mobx-react/__tests__/symbol.test.tsx b/packages/mobx-react/__tests__/symbol.test.tsx new file mode 100644 index 0000000000..4fe06ba45b --- /dev/null +++ b/packages/mobx-react/__tests__/symbol.test.tsx @@ -0,0 +1,25 @@ +import React from "react" +import { observer } from "../src" +import { render } from "@testing-library/react" +import { newSymbol } from "../src/utils/utils" + +// @ts-ignore +delete global.Symbol + +test("work without Symbol", () => { + const Component1 = observer( + class extends React.Component { + render() { + return null + } + } + ) + render() +}) + +test("cache newSymbol created Symbols", () => { + const symbol1 = newSymbol("name") + const symbol2 = newSymbol("name") + + expect(symbol1).toEqual(symbol2) +}) diff --git a/packages/mobx-react/__tests__/transactions.test.tsx b/packages/mobx-react/__tests__/transactions.test.tsx new file mode 100644 index 0000000000..585abb6760 --- /dev/null +++ b/packages/mobx-react/__tests__/transactions.test.tsx @@ -0,0 +1,84 @@ +import React from "react" +import { autorun, computed, observable, transaction } from "mobx" +import { observer } from "../src" +import { render } from "@testing-library/react" + +test("mobx issue 50", async () => { + const foo = { + a: observable.box(true), + b: observable.box(false), + c: computed(function () { + // console.log("evaluate c") + return foo.b.get() + }) + } + function flipStuff() { + transaction(() => { + foo.a.set(!foo.a.get()) + foo.b.set(!foo.b.get()) + }) + } + let asText = "" + autorun(() => (asText = [foo.a.get(), foo.b.get(), foo.c.get()].join(":"))) + const Test = observer( + class Test extends React.Component { + render() { + return
    {[foo.a.get(), foo.b.get(), foo.c.get()].join(",")}
    + } + } + ) + + render() + + // Flip a and b. This will change c. + flipStuff() + + expect(asText).toBe("false:true:true") + expect(document.getElementById("x")!.innerHTML).toBe("false,true,true") +}) + +test("ReactDOM.render should respect transaction", () => { + const a = observable.box(2) + const loaded = observable.box(false) + const valuesSeen: Array = [] + + const Component = observer(() => { + valuesSeen.push(a.get()) + if (loaded.get()) return
    {a.get()}
    + else return
    loading
    + }) + + const { container } = render() + + transaction(() => { + a.set(3) + a.set(4) + loaded.set(true) + }) + + expect(container.textContent).toBe("4") + expect(valuesSeen.sort()).toEqual([2, 4].sort()) +}) + +test("ReactDOM.render in transaction should succeed", () => { + const a = observable.box(2) + const loaded = observable.box(false) + const valuesSeen: Array = [] + const Component = observer(() => { + valuesSeen.push(a.get()) + if (loaded.get()) return
    {a.get()}
    + else return
    loading
    + }) + + let container + + transaction(() => { + a.set(3) + container = render().container + a.set(4) + loaded.set(true) + }) + + expect(container.textContent).toBe("4") + expect(valuesSeen.sort()).toEqual([3, 4].sort()) +}) diff --git a/packages/mobx-react/__tests__/utils/ErrorCatcher.tsx b/packages/mobx-react/__tests__/utils/ErrorCatcher.tsx new file mode 100644 index 0000000000..3f6e190ecb --- /dev/null +++ b/packages/mobx-react/__tests__/utils/ErrorCatcher.tsx @@ -0,0 +1,35 @@ +import React from "react" + +interface ErrorCatcherState { + hasError: boolean +} + +// FIXME: saddly, this does not work as hoped, see: https://github.com/facebook/react/issues/10474#issuecomment-332810203 +export default class ErrorCatcher extends React.Component> { + static lastError + static getError + constructor(props) { + super(props) + this.state = { hasError: false } + } + + componentDidCatch(error, info) { + console.error("Caught react error", error, info) + ErrorCatcher.lastError = "" + error + this.setState({ hasError: true }) + } + + render() { + if (this.state.hasError) { + return null + } + return this.props.children + } +} + +ErrorCatcher.lastError = "" +ErrorCatcher.getError = function () { + const res = ErrorCatcher.lastError + ErrorCatcher.lastError = "" + return res +} diff --git a/packages/mobx-react/__tests__/utils/compile-ts.tsx b/packages/mobx-react/__tests__/utils/compile-ts.tsx new file mode 100644 index 0000000000..022f599172 --- /dev/null +++ b/packages/mobx-react/__tests__/utils/compile-ts.tsx @@ -0,0 +1,289 @@ +import React from "react" +import ReactDOM from "react-dom" +import PropTypes from "prop-types" +import { + observer, + Provider, + inject, + Observer, + disposeOnUnmount, + PropTypes as MRPropTypes, + useLocalStore +} from "../src" + +@observer +class T1 extends React.Component<{ pizza: number }, {}> { + render() { + return
    {this.props.pizza}
    + } +} + +const T2 = observer( + class T2 extends React.Component<{ cake: number; zoem: any[] }> { + defaultProps = { cake: 7 } + render() { + return ( +
    + +
    + ) + } + static propTypes = { + zoem: MRPropTypes.arrayOrObservableArray + } + } +) + +const T3 = observer((props: { hamburger: number }) => { + return +}) + +const T4 = ({ sandwich }: { sandwich: number }) => ( +
    + +
    +) + +const T5 = observer(() => { + return +}) + +@observer +class T6 extends React.Component<{}, {}> { + render() { + return ( + + + {/* doesn't work with tsc 1.7.5: https://github.com/Microsoft/TypeScript/issues/5675 */} + {/**/} + + + ) + } +} + +const x = React.createElement(T3, { hamburger: 4 }) + +class T7 extends React.Component<{ pizza: number }, {}> { + render() { + return
    {this.props.pizza}
    + } +} +React.createElement(observer(T7), { pizza: 4 }) + +ReactDOM.render(, document.body) + +class ProviderTest extends React.Component { + render() { + return ( + +
    hi
    +
    + ) + } +} + +@inject(() => ({ x: 3 })) +class T11 extends React.Component<{ pizza: number; x?: number }, {}> { + render() { + return ( +
    + {this.props.pizza} + {this.props.x} +
    + ) + } +} + +class T15 extends React.Component<{ pizza: number; x?: number }, {}> { + render() { + return ( +
    + {this.props.pizza} + {this.props.x} +
    + ) + } +} +const T16 = inject(() => ({ x: 3 }))(T15) + +class T17 extends React.Component<{}, {}> { + render() { + return ( +
    + + + + + + +
    + ) + } +} + +@inject("a", "b") +class T12 extends React.Component<{ pizza: number }, {}> { + render() { + return
    {this.props.pizza}
    + } +} + +@inject("a", "b") +@observer +class T13 extends React.Component<{ pizza: number }, {}> { + render() { + return
    {this.props.pizza}
    + } +} + +const LoginContainer = inject((allStores, props) => ({ + store: { y: true, z: 2 }, + z: 7 +}))( + observer( + class _LoginContainer extends React.Component< + { + x: string + store?: { y: boolean; z: number } + }, + {} + > { + static contextTypes: React.ValidationMap = { + router: PropTypes.func.isRequired + } + + render() { + return ( +
    + Hello! + {this.props.x} + {this.props.store!.y} +
    + ) + } + } + ) +) +ReactDOM.render(, document.body) + +@inject(allStores => ({ + store: { y: true, z: 2 } +})) +@observer +class LoginContainer2 extends React.Component< + { + x: string + store?: { y: boolean } + }, + {} +> { + static contextTypes: React.ValidationMap = { + router: PropTypes.func.isRequired + } + + render() { + return ( +
    + Hello! + {this.props.x} + {this.props.store!.y} +
    + ) + } +} + +ReactDOM.render(, document.body) + +class ObserverTest extends React.Component { + render() { + return {() =>
    test
    }
    + } +} + +class ObserverTest2 extends React.Component { + render() { + return
    test
    } /> + } +} + +@observer +class ComponentWithoutPropsAndState extends React.Component<{}, {}> { + componentDidUpdate() {} + + render() { + return
    Hello!
    + } +} + +const AppInner = observer((props: { a: number }) => { + return ( +
    +

    Hello

    + {props.a} +
    + ) +}) + +const App = inject("store")(AppInner) + +App.wrappedComponent + +@inject("store") +@observer +class App2 extends React.Component<{ a: number }, {}> {} + +class InjectSomeStores extends React.Component<{ x: any }, {}> { + render() { + return
    Hello World
    + } +} + +inject(({ x }) => ({ x }))(InjectSomeStores) + +{ + class T extends React.Component<{ x: number }> { + render() { + return
    + } + } + + const Injected = inject("test")(T) + ; +} + +{ + // just to make sure it compiles + class DisposeOnUnmountComponent extends React.Component<{}> { + @disposeOnUnmount + methodA = () => {} + + methodB = disposeOnUnmount(this, () => {}) + manyMethods = disposeOnUnmount(this, [() => {}, () => {}]) + } + + // manual tests: this should not compile when the decorator is not applied over a react component class + /* + class DisposeOnUnmountNotAComponent { + @disposeOnUnmount + methodA = () => {} + + methodB = disposeOnUnmount(this, () => {}) + } + */ +} + +{ + const TestComponent = () => { + const observable = useLocalStore(() => ({ + test: 3 + })) + + return

    {observable.test * 2}

    + } + ; +} + +test("ok", () => { + // just to satisfy jest +}) diff --git a/packages/mobx-react/__tests__/utils/withConsole.ts b/packages/mobx-react/__tests__/utils/withConsole.ts new file mode 100644 index 0000000000..56009c2552 --- /dev/null +++ b/packages/mobx-react/__tests__/utils/withConsole.ts @@ -0,0 +1,22 @@ +import mockConsole, { MockObj } from "jest-mock-console" + +export function withConsole(fn: Function): void +export function withConsole(settings: MockObj, fn: Function): void +export function withConsole(props: Array, fn: Function): void + +export function withConsole(...args: Array): void { + let settings + let fn + if (typeof args[0] === "function") { + fn = args[0] + } else if (Array.isArray(args[0]) || typeof args[0] === "object") { + settings = args[0] + + if (typeof args[1] === "function") { + fn = args[1] + } + } + const restoreConsole = mockConsole(settings) + fn && fn() + restoreConsole() +} diff --git a/packages/mobx-react/batchingForReactDom.js b/packages/mobx-react/batchingForReactDom.js new file mode 100644 index 0000000000..3c73786c62 --- /dev/null +++ b/packages/mobx-react/batchingForReactDom.js @@ -0,0 +1 @@ +require("mobx-react-lite/batchingForReactDom") diff --git a/packages/mobx-react/batchingForReactNative.js b/packages/mobx-react/batchingForReactNative.js new file mode 100644 index 0000000000..f6f328548a --- /dev/null +++ b/packages/mobx-react/batchingForReactNative.js @@ -0,0 +1 @@ +require("mobx-react-lite/batchingForReactNative") diff --git a/packages/mobx-react/batchingOptOut.js b/packages/mobx-react/batchingOptOut.js new file mode 100644 index 0000000000..f9533d9921 --- /dev/null +++ b/packages/mobx-react/batchingOptOut.js @@ -0,0 +1 @@ +require("mobx-react-lite/batchingOptOut") diff --git a/packages/mobx-react/jest.config.js b/packages/mobx-react/jest.config.js new file mode 100644 index 0000000000..9a49a001f2 --- /dev/null +++ b/packages/mobx-react/jest.config.js @@ -0,0 +1,7 @@ +const buildConfig = require("../../jest.base.config") + +module.exports = buildConfig(__dirname, { + testRegex: "__tests__/.*\\.tsx$", + setupFilesAfterEnv: [`/jest.setup.ts`], + testPathIgnorePatterns: ["node_modules", "/__tests__/utils"] +}) diff --git a/packages/mobx-react/jest.setup.ts b/packages/mobx-react/jest.setup.ts new file mode 100644 index 0000000000..99fecb2566 --- /dev/null +++ b/packages/mobx-react/jest.setup.ts @@ -0,0 +1,16 @@ +import "@testing-library/jest-dom/extend-expect" +import { configure } from "mobx" + +configure({ enforceActions: "never" }) + +// @ts-ignore +global.__DEV__ = true + +// Uglyness to find missing 'act' more easily +// 14-2-19 / React 16.8.1, temporarily work around, as error message misses a stack-trace +Error.stackTraceLimit = Infinity +const origError = console.error +console.error = function (msg) { + if (/react-wrap-tests-with-act/.test("" + msg)) throw new Error("missing act") + return origError.apply(this, arguments as any) +} diff --git a/packages/mobx-react/package.json b/packages/mobx-react/package.json new file mode 100644 index 0000000000..7b451c8566 --- /dev/null +++ b/packages/mobx-react/package.json @@ -0,0 +1,75 @@ +{ + "name": "mobx-react", + "version": "7.0.0", + "description": "React bindings for MobX. Create fully reactive components.", + "source": "src/index.ts", + "main": "dist/index.js", + "umd:main": "dist/mobxreact.umd.production.min.js", + "unpkg": "dist/mobxreact.umd.production.min.js", + "jsnext:main": "dist/mobxreact.esm.js", + "module": "dist/mobxreact.esm.js", + "react-native": "dist/mobxreact.esm.js", + "types": "dist/index.d.ts", + "files": [ + "src", + "dist", + "LICENSE", + "CHANGELOG.md", + "README.md", + "batching*" + ], + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/mobxjs/mobx.git" + }, + "author": "Michel Weststrate", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "bugs": { + "url": "https://github.com/mobxjs/mobx/issues" + }, + "homepage": "http://mobx.js.org/", + "dependencies": { + "mobx-react-lite": "^3.0.0" + }, + "peerDependencies": { + "mobx": "^6.0.0", + "react": "^16.8.0 || ^17" + }, + "devDependencies": { + "@changesets/changelog-github": "^0.2.7", + "@changesets/cli": "^2.11.0", + "@testing-library/jest-dom": "^5.1.1", + "@testing-library/react": "^9.4.0", + "@types/node": "^10.0.0", + "@types/prop-types": "^15.5.2", + "@types/react": "^16.8.24", + "@types/react-dom": "^16.0.5", + "lodash": "^4.17.4", + "mobx": "^6.0.0", + "prop-types": "^15.7.2", + "react": "^16.9.0", + "react-dom": "^16.9.0" + }, + "keywords": [ + "mobx", + "mobservable", + "react-component", + "react", + "reactjs", + "reactive" + ], + "scripts": { + "lint": "eslint src/**/*", + "build": "node ../../scripts/build.js mobx-react", + "test": "jest", + "test:types": "yarn tsc --noEmit", + "test:size": "yarn import-size --report . observer", + "test:coverage": "jest -i --coverage", + "prepublish": "yarn build --target publish" + } +} diff --git a/packages/mobx-react/src/Provider.tsx b/packages/mobx-react/src/Provider.tsx new file mode 100644 index 0000000000..b40f1da728 --- /dev/null +++ b/packages/mobx-react/src/Provider.tsx @@ -0,0 +1,29 @@ +import React from "react" +import { shallowEqual } from "./utils/utils" +import { IValueMap } from "./types/IValueMap" + +export const MobXProviderContext = React.createContext({}) + +export interface ProviderProps extends IValueMap { + children: React.ReactNode +} + +export function Provider(props: ProviderProps) { + const { children, ...stores } = props + const parentValue = React.useContext(MobXProviderContext) + const mutableProviderRef = React.useRef({ ...parentValue, ...stores }) + const value = mutableProviderRef.current + + if (__DEV__) { + const newValue = { ...value, ...stores } // spread in previous state for the context based stores + if (!shallowEqual(value, newValue)) { + throw new Error( + "MobX Provider: The set of provided stores has changed. See: https://github.com/mobxjs/mobx-react#the-set-of-provided-stores-has-changed-error." + ) + } + } + + return {children} +} + +Provider.displayName = "MobXProvider" diff --git a/packages/mobx-react/src/disposeOnUnmount.ts b/packages/mobx-react/src/disposeOnUnmount.ts new file mode 100644 index 0000000000..409273d6f7 --- /dev/null +++ b/packages/mobx-react/src/disposeOnUnmount.ts @@ -0,0 +1,85 @@ +import React from "react" +import { patch, newSymbol } from "./utils/utils" + +type Disposer = () => void + +const protoStoreKey = newSymbol("disposeOnUnmountProto") +const instStoreKey = newSymbol("disposeOnUnmountInst") + +function runDisposersOnWillUnmount() { + ;[...(this[protoStoreKey] || []), ...(this[instStoreKey] || [])].forEach(propKeyOrFunction => { + const prop = + typeof propKeyOrFunction === "string" ? this[propKeyOrFunction] : propKeyOrFunction + if (prop !== undefined && prop !== null) { + if (Array.isArray(prop)) prop.map(f => f()) + else prop() + } + }) +} + +export function disposeOnUnmount(target: React.Component, propertyKey: PropertyKey): void +export function disposeOnUnmount>( + target: React.Component, + fn: TF +): TF + +export function disposeOnUnmount( + target: React.Component, + propertyKeyOrFunction: PropertyKey | Disposer | Array +): PropertyKey | Disposer | Array | void { + if (Array.isArray(propertyKeyOrFunction)) { + return propertyKeyOrFunction.map(fn => disposeOnUnmount(target, fn)) + } + + const c = Object.getPrototypeOf(target).constructor + const c2 = Object.getPrototypeOf(target.constructor) + // Special case for react-hot-loader + const c3 = Object.getPrototypeOf(Object.getPrototypeOf(target)) + if ( + !( + c === React.Component || + c === React.PureComponent || + c2 === React.Component || + c2 === React.PureComponent || + c3 === React.Component || + c3 === React.PureComponent + ) + ) { + throw new Error( + "[mobx-react] disposeOnUnmount only supports direct subclasses of React.Component or React.PureComponent." + ) + } + + if ( + typeof propertyKeyOrFunction !== "string" && + typeof propertyKeyOrFunction !== "function" && + !Array.isArray(propertyKeyOrFunction) + ) { + throw new Error( + "[mobx-react] disposeOnUnmount only works if the parameter is either a property key or a function." + ) + } + + // decorator's target is the prototype, so it doesn't have any instance properties like props + const isDecorator = typeof propertyKeyOrFunction === "string" + + // add property key / function we want run (disposed) to the store + const componentWasAlreadyModified = !!target[protoStoreKey] || !!target[instStoreKey] + const store = isDecorator + ? // decorators are added to the prototype store + target[protoStoreKey] || (target[protoStoreKey] = []) + : // functions are added to the instance store + target[instStoreKey] || (target[instStoreKey] = []) + + store.push(propertyKeyOrFunction) + + // tweak the component class componentWillUnmount if not done already + if (!componentWasAlreadyModified) { + patch(target, "componentWillUnmount", runDisposersOnWillUnmount) + } + + // return the disposer as is if invoked as a non decorator + if (typeof propertyKeyOrFunction !== "string") { + return propertyKeyOrFunction + } +} diff --git a/packages/mobx-react/src/globals.d.ts b/packages/mobx-react/src/globals.d.ts new file mode 100644 index 0000000000..404c55f1ee --- /dev/null +++ b/packages/mobx-react/src/globals.d.ts @@ -0,0 +1 @@ +declare const __DEV__: boolean diff --git a/packages/mobx-react/src/index.ts b/packages/mobx-react/src/index.ts new file mode 100644 index 0000000000..6bdd979296 --- /dev/null +++ b/packages/mobx-react/src/index.ts @@ -0,0 +1,25 @@ +import { observable } from "mobx" +import { Component } from "react" + +if (!Component) throw new Error("mobx-react requires React to be available") +if (!observable) throw new Error("mobx-react requires mobx to be available") + +export { + Observer, + useObserver, + useAsObservableSource, + useLocalStore, + isUsingStaticRendering, + useStaticRendering, + enableStaticRendering, + observerBatching, + useLocalObservable +} from "mobx-react-lite" + +export { observer } from "./observer" + +export { MobXProviderContext, Provider, ProviderProps } from "./Provider" +export { inject } from "./inject" +export { disposeOnUnmount } from "./disposeOnUnmount" +export { PropTypes } from "./propTypes" +export { IWrappedComponent } from "./types/IWrappedComponent" diff --git a/packages/mobx-react/src/inject.ts b/packages/mobx-react/src/inject.ts new file mode 100644 index 0000000000..618b2ace27 --- /dev/null +++ b/packages/mobx-react/src/inject.ts @@ -0,0 +1,104 @@ +import React from "react" +import { observer } from "./observer" +import { copyStaticProperties } from "./utils/utils" +import { MobXProviderContext } from "./Provider" +import { IReactComponent } from "./types/IReactComponent" +import { IValueMap } from "./types/IValueMap" +import { IWrappedComponent } from "./types/IWrappedComponent" +import { IStoresToProps } from "./types/IStoresToProps" + +/** + * Store Injection + */ +function createStoreInjector( + grabStoresFn: IStoresToProps, + component: IReactComponent, + injectNames: string, + makeReactive: boolean +): IReactComponent { + // Support forward refs + let Injector: IReactComponent = React.forwardRef((props, ref) => { + const newProps = { ...props } + const context = React.useContext(MobXProviderContext) + Object.assign(newProps, grabStoresFn(context || {}, newProps) || {}) + + if (ref) { + newProps.ref = ref + } + + return React.createElement(component, newProps) + }) + + if (makeReactive) Injector = observer(Injector) + Injector["isMobxInjector"] = true // assigned late to suppress observer warning + + // Static fields from component should be visible on the generated Injector + copyStaticProperties(component, Injector) + Injector["wrappedComponent"] = component + Injector.displayName = getInjectName(component, injectNames) + return Injector +} + +function getInjectName(component: IReactComponent, injectNames: string): string { + let displayName + const componentName = + component.displayName || + component.name || + (component.constructor && component.constructor.name) || + "Component" + if (injectNames) displayName = "inject-with-" + injectNames + "(" + componentName + ")" + else displayName = "inject(" + componentName + ")" + return displayName +} + +function grabStoresByName( + storeNames: Array +): (baseStores: IValueMap, nextProps: React.Props) => React.PropsWithRef | undefined { + return function (baseStores, nextProps) { + storeNames.forEach(function (storeName) { + if ( + storeName in nextProps // prefer props over stores + ) + return + if (!(storeName in baseStores)) + throw new Error( + "MobX injector: Store '" + + storeName + + "' is not available! Make sure it is provided by some Provider" + ) + nextProps[storeName] = baseStores[storeName] + }) + return nextProps + } +} + +export function inject( + ...stores: Array +): >( + target: T +) => T & (T extends IReactComponent ? IWrappedComponent

    : never) +export function inject( + fn: IStoresToProps +): (target: T) => T & IWrappedComponent

    + +/** + * higher order component that injects stores to a child. + * takes either a varargs list of strings, which are stores read from the context, + * or a function that manually maps the available stores from the context to props: + * storesToProps(mobxStores, props, context) => newProps + */ +export function inject(/* fn(stores, nextProps) or ...storeNames */ ...storeNames: Array) { + if (typeof arguments[0] === "function") { + let grabStoresFn = arguments[0] + return (componentClass: React.ComponentClass) => + createStoreInjector(grabStoresFn, componentClass, grabStoresFn.name, true) + } else { + return (componentClass: React.ComponentClass) => + createStoreInjector( + grabStoresByName(storeNames), + componentClass, + storeNames.join("-"), + false + ) + } +} diff --git a/packages/mobx-react/src/observer.tsx b/packages/mobx-react/src/observer.tsx new file mode 100644 index 0000000000..8437373e4a --- /dev/null +++ b/packages/mobx-react/src/observer.tsx @@ -0,0 +1,58 @@ +import * as React from "react" +import { observer as observerLite, Observer } from "mobx-react-lite" + +import { makeClassComponentObserver } from "./observerClass" +import { IReactComponent } from "./types/IReactComponent" + +const hasSymbol = typeof Symbol === "function" && Symbol.for + +// Using react-is had some issues (and operates on elements, not on types), see #608 / #609 +const ReactForwardRefSymbol = hasSymbol + ? Symbol.for("react.forward_ref") + : typeof React.forwardRef === "function" && React.forwardRef((props: any) => null)["$$typeof"] + +const ReactMemoSymbol = hasSymbol + ? Symbol.for("react.memo") + : typeof React.memo === "function" && React.memo((props: any) => null)["$$typeof"] + +/** + * Observer function / decorator + */ +export function observer(component: T): T { + if (component["isMobxInjector"] === true) { + console.warn( + "Mobx observer: You are trying to use 'observer' on a component that already has 'inject'. Please apply 'observer' before applying 'inject'" + ) + } + + if (ReactMemoSymbol && component["$$typeof"] === ReactMemoSymbol) { + throw new Error( + "Mobx observer: You are trying to use 'observer' on a function component wrapped in either another observer or 'React.memo'. The observer already applies 'React.memo' for you." + ) + } + + // Unwrap forward refs into `` component + // we need to unwrap the render, because it is the inner render that needs to be tracked, + // not the ForwardRef HoC + if (ReactForwardRefSymbol && component["$$typeof"] === ReactForwardRefSymbol) { + const baseRender = component["render"] + if (typeof baseRender !== "function") + throw new Error("render property of ForwardRef was not a function") + return React.forwardRef(function ObserverForwardRef() { + const args = arguments + return {() => baseRender.apply(undefined, args)} + }) as T + } + + // Function component + if ( + typeof component === "function" && + (!component.prototype || !component.prototype.render) && + !component["isReactClass"] && + !Object.prototype.isPrototypeOf.call(React.Component, component) + ) { + return observerLite(component as React.StatelessComponent) as T + } + + return makeClassComponentObserver(component as React.ComponentClass) as T +} diff --git a/packages/mobx-react/src/observerClass.ts b/packages/mobx-react/src/observerClass.ts new file mode 100644 index 0000000000..7fddc23c43 --- /dev/null +++ b/packages/mobx-react/src/observerClass.ts @@ -0,0 +1,202 @@ +import { PureComponent, Component } from "react" +import { + createAtom, + _allowStateChanges, + Reaction, + $mobx, + _allowStateReadsStart, + _allowStateReadsEnd +} from "mobx" +import { isUsingStaticRendering } from "mobx-react-lite" + +import { newSymbol, shallowEqual, setHiddenProp, patch } from "./utils/utils" + +const mobxAdminProperty = $mobx || "$mobx" +const mobxObserverProperty = newSymbol("isMobXReactObserver") +const mobxIsUnmounted = newSymbol("isUnmounted") +const skipRenderKey = newSymbol("skipRender") +const isForcingUpdateKey = newSymbol("isForcingUpdate") + +export function makeClassComponentObserver( + componentClass: React.ComponentClass +): React.ComponentClass { + const target = componentClass.prototype + + if (componentClass[mobxObserverProperty]) { + const displayName = getDisplayName(target) + console.warn( + `The provided component class (${displayName}) + has already been declared as an observer component.` + ) + } else { + componentClass[mobxObserverProperty] = true + } + + if (target.componentWillReact) + throw new Error("The componentWillReact life-cycle event is no longer supported") + if (componentClass["__proto__"] !== PureComponent) { + if (!target.shouldComponentUpdate) target.shouldComponentUpdate = observerSCU + else if (target.shouldComponentUpdate !== observerSCU) + // n.b. unequal check, instead of existence check, as @observer might be on superclass as well + throw new Error( + "It is not allowed to use shouldComponentUpdate in observer based components." + ) + } + + // this.props and this.state are made observable, just to make sure @computed fields that + // are defined inside the component, and which rely on state or props, re-compute if state or props change + // (otherwise the computed wouldn't update and become stale on props change, since props are not observable) + // However, this solution is not without it's own problems: https://github.com/mobxjs/mobx-react/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aobservable-props-or-not+ + makeObservableProp(target, "props") + makeObservableProp(target, "state") + + const baseRender = target.render + target.render = function () { + return makeComponentReactive.call(this, baseRender) + } + patch(target, "componentWillUnmount", function () { + if (isUsingStaticRendering() === true) return + this.render[mobxAdminProperty]?.dispose() + this[mobxIsUnmounted] = true + + if (!this.render[mobxAdminProperty]) { + // Render may have been hot-swapped and/or overriden by a subclass. + const displayName = getDisplayName(this) + console.warn( + `The reactive render of an observer class component (${displayName}) + was overriden after MobX attached. This may result in a memory leak if the + overriden reactive render was not properly disposed.` + ) + } + }) + return componentClass +} + +// Generates a friendly name for debugging +function getDisplayName(comp: any) { + return ( + comp.displayName || + comp.name || + (comp.constructor && (comp.constructor.displayName || comp.constructor.name)) || + "" + ) +} + +function makeComponentReactive(render: any) { + if (isUsingStaticRendering() === true) return render.call(this) + + /** + * If props are shallowly modified, react will render anyway, + * so atom.reportChanged() should not result in yet another re-render + */ + setHiddenProp(this, skipRenderKey, false) + /** + * forceUpdate will re-assign this.props. We don't want that to cause a loop, + * so detect these changes + */ + setHiddenProp(this, isForcingUpdateKey, false) + + const initialName = getDisplayName(this) + const baseRender = render.bind(this) + + let isRenderingPending = false + + const reaction = new Reaction(`${initialName}.render()`, () => { + if (!isRenderingPending) { + // N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.js) + // This unidiomatic React usage but React will correctly warn about this so we continue as usual + // See #85 / Pull #44 + isRenderingPending = true + if (this[mobxIsUnmounted] !== true) { + let hasError = true + try { + setHiddenProp(this, isForcingUpdateKey, true) + if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this) + hasError = false + } finally { + setHiddenProp(this, isForcingUpdateKey, false) + if (hasError) reaction.dispose() + } + } + } + }) + + reaction["reactComponent"] = this + reactiveRender[mobxAdminProperty] = reaction + this.render = reactiveRender + + function reactiveRender() { + isRenderingPending = false + let exception = undefined + let rendering = undefined + reaction.track(() => { + try { + rendering = _allowStateChanges(false, baseRender) + } catch (e) { + exception = e + } + }) + if (exception) { + throw exception + } + return rendering + } + + return reactiveRender.call(this) +} + +function observerSCU(nextProps: React.Props, nextState: any): boolean { + if (isUsingStaticRendering()) { + console.warn( + "[mobx-react] It seems that a re-rendering of a React component is triggered while in static (server-side) mode. Please make sure components are rendered only once server-side." + ) + } + // update on any state changes (as is the default) + if (this.state !== nextState) { + return true + } + // update if props are shallowly not equal, inspired by PureRenderMixin + // we could return just 'false' here, and avoid the `skipRender` checks etc + // however, it is nicer if lifecycle events are triggered like usually, + // so we return true here if props are shallowly modified. + return !shallowEqual(this.props, nextProps) +} + +function makeObservableProp(target: any, propName: string): void { + const valueHolderKey = newSymbol(`reactProp_${propName}_valueHolder`) + const atomHolderKey = newSymbol(`reactProp_${propName}_atomHolder`) + function getAtom() { + if (!this[atomHolderKey]) { + setHiddenProp(this, atomHolderKey, createAtom("reactive " + propName)) + } + return this[atomHolderKey] + } + Object.defineProperty(target, propName, { + configurable: true, + enumerable: true, + get: function () { + let prevReadState = false + + if (_allowStateReadsStart && _allowStateReadsEnd) { + prevReadState = _allowStateReadsStart(true) + } + getAtom.call(this).reportObserved() + + if (_allowStateReadsStart && _allowStateReadsEnd) { + _allowStateReadsEnd(prevReadState) + } + + return this[valueHolderKey] + }, + set: function set(v) { + if (!this[isForcingUpdateKey] && !shallowEqual(this[valueHolderKey], v)) { + setHiddenProp(this, valueHolderKey, v) + setHiddenProp(this, skipRenderKey, true) + getAtom.call(this).reportChanged() + setHiddenProp(this, skipRenderKey, false) + } else { + setHiddenProp(this, valueHolderKey, v) + } + } + }) +} diff --git a/packages/mobx-react/src/propTypes.ts b/packages/mobx-react/src/propTypes.ts new file mode 100644 index 0000000000..0e6e164bce --- /dev/null +++ b/packages/mobx-react/src/propTypes.ts @@ -0,0 +1,210 @@ +import { isObservableArray, isObservableObject, isObservableMap, untracked } from "mobx" + +// Copied from React.PropTypes +function createChainableTypeChecker(validator: React.Validator): React.Requireable { + function checkType( + isRequired: boolean, + props: any, + propName: string, + componentName: string, + location: string, + propFullName: string, + ...rest: any[] + ) { + return untracked(() => { + componentName = componentName || "<>" + propFullName = propFullName || propName + if (props[propName] == null) { + if (isRequired) { + const actual = props[propName] === null ? "null" : "undefined" + return new Error( + "The " + + location + + " `" + + propFullName + + "` is marked as required " + + "in `" + + componentName + + "`, but its value is `" + + actual + + "`." + ) + } + return null + } else { + // @ts-ignore rest arg is necessary for some React internals - fails tests otherwise + return validator(props, propName, componentName, location, propFullName, ...rest) + } + }) + } + + const chainedCheckType: any = checkType.bind(null, false) + // Add isRequired to satisfy Requirable + chainedCheckType.isRequired = checkType.bind(null, true) + return chainedCheckType +} + +// Copied from React.PropTypes +function isSymbol(propType: any, propValue: any): boolean { + // Native Symbol. + if (propType === "symbol") { + return true + } + + // 19.4.3.5 Symbol.prototype[@@toStringTag] === 'Symbol' + if (propValue["@@toStringTag"] === "Symbol") { + return true + } + + // Fallback for non-spec compliant Symbols which are polyfilled. + if (typeof Symbol === "function" && propValue instanceof Symbol) { + return true + } + + return false +} + +// Copied from React.PropTypes +function getPropType(propValue: any): string { + const propType = typeof propValue + if (Array.isArray(propValue)) { + return "array" + } + if (propValue instanceof RegExp) { + // Old webkits (at least until Android 4.0) return 'function' rather than + // 'object' for typeof a RegExp. We'll normalize this here so that /bla/ + // passes PropTypes.object. + return "object" + } + if (isSymbol(propType, propValue)) { + return "symbol" + } + return propType +} + +// This handles more types than `getPropType`. Only used for error messages. +// Copied from React.PropTypes +function getPreciseType(propValue: any): string { + const propType = getPropType(propValue) + if (propType === "object") { + if (propValue instanceof Date) { + return "date" + } else if (propValue instanceof RegExp) { + return "regexp" + } + } + return propType +} + +function createObservableTypeCheckerCreator( + allowNativeType: any, + mobxType: any +): React.Requireable { + return createChainableTypeChecker((props, propName, componentName, location, propFullName) => { + return untracked(() => { + if (allowNativeType) { + if (getPropType(props[propName]) === mobxType.toLowerCase()) return null + } + let mobxChecker + switch (mobxType) { + case "Array": + mobxChecker = isObservableArray + break + case "Object": + mobxChecker = isObservableObject + break + case "Map": + mobxChecker = isObservableMap + break + default: + throw new Error(`Unexpected mobxType: ${mobxType}`) + } + const propValue = props[propName] + if (!mobxChecker(propValue)) { + const preciseType = getPreciseType(propValue) + const nativeTypeExpectationMessage = allowNativeType + ? " or javascript `" + mobxType.toLowerCase() + "`" + : "" + return new Error( + "Invalid prop `" + + propFullName + + "` of type `" + + preciseType + + "` supplied to" + + " `" + + componentName + + "`, expected `mobx.Observable" + + mobxType + + "`" + + nativeTypeExpectationMessage + + "." + ) + } + return null + }) + }) +} + +function createObservableArrayOfTypeChecker( + allowNativeType: boolean, + typeChecker: React.Validator +) { + return createChainableTypeChecker( + (props, propName, componentName, location, propFullName, ...rest) => { + return untracked(() => { + if (typeof typeChecker !== "function") { + return new Error( + "Property `" + + propFullName + + "` of component `" + + componentName + + "` has " + + "invalid PropType notation." + ) + } else { + let error = createObservableTypeCheckerCreator(allowNativeType, "Array")( + props, + propName, + componentName, + location, + propFullName + ) + + if (error instanceof Error) return error + const propValue = props[propName] + for (let i = 0; i < propValue.length; i++) { + error = (typeChecker as React.Validator)( + propValue, + i as any, + componentName, + location, + propFullName + "[" + i + "]", + ...rest + ) + if (error instanceof Error) return error + } + + return null + } + }) + } + ) +} + +const observableArray = createObservableTypeCheckerCreator(false, "Array") +const observableArrayOf = createObservableArrayOfTypeChecker.bind(null, false) +const observableMap = createObservableTypeCheckerCreator(false, "Map") +const observableObject = createObservableTypeCheckerCreator(false, "Object") +const arrayOrObservableArray = createObservableTypeCheckerCreator(true, "Array") +const arrayOrObservableArrayOf = createObservableArrayOfTypeChecker.bind(null, true) +const objectOrObservableObject = createObservableTypeCheckerCreator(true, "Object") + +export const PropTypes = { + observableArray, + observableArrayOf, + observableMap, + observableObject, + arrayOrObservableArray, + arrayOrObservableArrayOf, + objectOrObservableObject +} diff --git a/packages/mobx-react/src/types/IReactComponent.ts b/packages/mobx-react/src/types/IReactComponent.ts new file mode 100644 index 0000000000..f3d33230d7 --- /dev/null +++ b/packages/mobx-react/src/types/IReactComponent.ts @@ -0,0 +1,5 @@ +export type IReactComponent

    = + | React.ClassicComponentClass

    + | React.ComponentClass

    + | React.FunctionComponent

    + | React.ForwardRefExoticComponent

    diff --git a/packages/mobx-react/src/types/IStoresToProps.ts b/packages/mobx-react/src/types/IStoresToProps.ts new file mode 100644 index 0000000000..44a23a48a0 --- /dev/null +++ b/packages/mobx-react/src/types/IStoresToProps.ts @@ -0,0 +1,7 @@ +import { IValueMap } from "./IValueMap" +export type IStoresToProps< + S extends IValueMap = {}, + P extends IValueMap = {}, + I extends IValueMap = {}, + C extends IValueMap = {} +> = (stores: S, nextProps: P, context?: C) => I diff --git a/packages/mobx-react/src/types/IValueMap.ts b/packages/mobx-react/src/types/IValueMap.ts new file mode 100644 index 0000000000..f7f1ad6cb9 --- /dev/null +++ b/packages/mobx-react/src/types/IValueMap.ts @@ -0,0 +1 @@ +export type IValueMap = Record diff --git a/packages/mobx-react/src/types/IWrappedComponent.ts b/packages/mobx-react/src/types/IWrappedComponent.ts new file mode 100644 index 0000000000..72abbae0b7 --- /dev/null +++ b/packages/mobx-react/src/types/IWrappedComponent.ts @@ -0,0 +1,4 @@ +import { IReactComponent } from "./IReactComponent" +export type IWrappedComponent

    = { + wrappedComponent: IReactComponent

    +} diff --git a/packages/mobx-react/src/utils/utils.ts b/packages/mobx-react/src/utils/utils.ts new file mode 100644 index 0000000000..b23f1d12bc --- /dev/null +++ b/packages/mobx-react/src/utils/utils.ts @@ -0,0 +1,192 @@ +let symbolId = 0 +function createSymbol(name: string): symbol | string { + if (typeof Symbol === "function") { + return Symbol(name) + } + const symbol = `__$mobx-react ${name} (${symbolId})` + symbolId++ + return symbol +} + +const createdSymbols = {} +export function newSymbol(name: string): symbol | string { + if (!createdSymbols[name]) { + createdSymbols[name] = createSymbol(name) + } + return createdSymbols[name] +} + +export function shallowEqual(objA: any, objB: any): boolean { + //From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js + if (is(objA, objB)) return true + if (typeof objA !== "object" || objA === null || typeof objB !== "object" || objB === null) { + return false + } + const keysA = Object.keys(objA) + const keysB = Object.keys(objB) + if (keysA.length !== keysB.length) return false + for (let i = 0; i < keysA.length; i++) { + if (!Object.hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { + return false + } + } + return true +} + +function is(x: any, y: any): boolean { + // From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js + if (x === y) { + return x !== 0 || 1 / x === 1 / y + } else { + return x !== x && y !== y + } +} + +// based on https://github.com/mridgway/hoist-non-react-statics/blob/master/src/index.js +const hoistBlackList = { + $$typeof: 1, + render: 1, + compare: 1, + type: 1, + childContextTypes: 1, + contextType: 1, + contextTypes: 1, + defaultProps: 1, + getDefaultProps: 1, + getDerivedStateFromError: 1, + getDerivedStateFromProps: 1, + mixins: 1, + propTypes: 1 +} + +export function copyStaticProperties(base: object, target: object): void { + const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(base)) + Object.getOwnPropertyNames(base).forEach(key => { + if (!hoistBlackList[key] && protoProps.indexOf(key) === -1) { + Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(base, key)!) + } + }) +} + +/** + * Helper to set `prop` to `this` as non-enumerable (hidden prop) + * @param target + * @param prop + * @param value + */ +export function setHiddenProp(target: object, prop: any, value: any): void { + if (!Object.hasOwnProperty.call(target, prop)) { + Object.defineProperty(target, prop, { + enumerable: false, + configurable: true, + writable: true, + value + }) + } else { + target[prop] = value + } +} + +/** + * Utilities for patching componentWillUnmount, to make sure @disposeOnUnmount works correctly icm with user defined hooks + * and the handler provided by mobx-react + */ +const mobxMixins = newSymbol("patchMixins") +const mobxPatchedDefinition = newSymbol("patchedDefinition") + +export interface Mixins extends Record { + locks: number + methods: Array +} + +function getMixins(target: object, methodName: string): Mixins { + const mixins = (target[mobxMixins] = target[mobxMixins] || {}) + const methodMixins = (mixins[methodName] = mixins[methodName] || {}) + methodMixins.locks = methodMixins.locks || 0 + methodMixins.methods = methodMixins.methods || [] + return methodMixins +} + +function wrapper(realMethod: Function, mixins: Mixins, ...args: Array) { + // locks are used to ensure that mixins are invoked only once per invocation, even on recursive calls + mixins.locks++ + + try { + let retVal + if (realMethod !== undefined && realMethod !== null) { + retVal = realMethod.apply(this, args) + } + + return retVal + } finally { + mixins.locks-- + if (mixins.locks === 0) { + mixins.methods.forEach(mx => { + mx.apply(this, args) + }) + } + } +} + +function wrapFunction(realMethod: Function, mixins: Mixins): (...args: Array) => any { + const fn = function (...args: Array) { + wrapper.call(this, realMethod, mixins, ...args) + } + return fn +} + +export function patch(target: object, methodName: string, mixinMethod: Function): void { + const mixins = getMixins(target, methodName) + + if (mixins.methods.indexOf(mixinMethod) < 0) { + mixins.methods.push(mixinMethod) + } + + const oldDefinition = Object.getOwnPropertyDescriptor(target, methodName) + if (oldDefinition && oldDefinition[mobxPatchedDefinition]) { + // already patched definition, do not repatch + return + } + + const originalMethod = target[methodName] + const newDefinition = createDefinition( + target, + methodName, + oldDefinition ? oldDefinition.enumerable : undefined, + mixins, + originalMethod + ) + + Object.defineProperty(target, methodName, newDefinition) +} + +function createDefinition( + target: object, + methodName: string, + enumerable: any, + mixins: Mixins, + originalMethod: Function +): PropertyDescriptor { + let wrappedFunc = wrapFunction(originalMethod, mixins) + + return { + [mobxPatchedDefinition]: true, + get: function () { + return wrappedFunc + }, + set: function (value) { + if (this === target) { + wrappedFunc = wrapFunction(value, mixins) + } else { + // when it is an instance of the prototype/a child prototype patch that particular case again separately + // since we need to store separate values depending on wether it is the actual instance, the prototype, etc + // e.g. the method for super might not be the same as the method for the prototype which might be not the same + // as the method for the instance + const newDefinition = createDefinition(this, methodName, enumerable, mixins, value) + Object.defineProperty(this, methodName, newDefinition) + } + }, + configurable: true, + enumerable: enumerable + } +} diff --git a/packages/mobx-react/tsconfig.json b/packages/mobx-react/tsconfig.json new file mode 100644 index 0000000000..d494a4775d --- /dev/null +++ b/packages/mobx-react/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "lib": ["es6", "DOM"] + }, + "include": ["src"] +} diff --git a/packages/mobx-react/tsconfig.test.json b/packages/mobx-react/tsconfig.test.json new file mode 100644 index 0000000000..7bf229ec27 --- /dev/null +++ b/packages/mobx-react/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.test.json", + "compilerOptions": { + "lib": ["esnext", "dom"] + } +} diff --git a/packages/mobx-react/tsdx.config.js b/packages/mobx-react/tsdx.config.js new file mode 100644 index 0000000000..07d57bd69f --- /dev/null +++ b/packages/mobx-react/tsdx.config.js @@ -0,0 +1,16 @@ +module.exports = { + rollup(config) { + return { + ...config, + output: { + ...config.output, + globals: { + react: "React", + mobx: "mobx", + "react-dom": "ReactDOM", + "mobx-react-lite": "mobxReactLite" + } + } + } + } +} diff --git a/packages/mobx-undecorate/__tests__/cli.spec.tsx b/packages/mobx-undecorate/__tests__/cli.spec.tsx index dc8e6595b4..41df616681 100644 --- a/packages/mobx-undecorate/__tests__/cli.spec.tsx +++ b/packages/mobx-undecorate/__tests__/cli.spec.tsx @@ -1,6 +1,6 @@ import { execSync } from "child_process" -import { join } from "path" -import { readFileSync, writeFileSync } from "fs" +import { join, dirname } from "path" +import { readFileSync, writeFileSync, mkdirSync } from "fs" const dedent = require("dedent-js") test("run cli #2506", () => { @@ -11,6 +11,7 @@ test("run cli #2506", () => { } `) + mkdirSync(dirname(testFile)) writeFileSync(testFile, baseContent) execSync("node ../../cli.js", { cwd: join(__dirname, "fixtures") diff --git a/packages/mobx-undecorate/__tests__/fixtures/.gitignore b/packages/mobx-undecorate/__tests__/fixtures/.gitignore new file mode 100644 index 0000000000..dfa66db341 --- /dev/null +++ b/packages/mobx-undecorate/__tests__/fixtures/.gitignore @@ -0,0 +1 @@ +*some* \ No newline at end of file diff --git a/packages/mobx-undecorate/__tests__/fixtures/some path/some file.tsx b/packages/mobx-undecorate/__tests__/fixtures/some path/some file.tsx deleted file mode 100644 index ba990cf36e..0000000000 --- a/packages/mobx-undecorate/__tests__/fixtures/some path/some file.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { observable, makeObservable } from "mobx" -class Test { - x = 1 - - constructor() { - makeObservable(this, { - x: observable - }) - } -} diff --git a/packages/mobx/package.json b/packages/mobx/package.json index da14bf0aad..6bb564b50c 100644 --- a/packages/mobx/package.json +++ b/packages/mobx/package.json @@ -69,6 +69,6 @@ "test:flow": "flow check", "test:coverage": "yarn test -i --coverage", "test:size": "yarn import-size --report . observable computed autorun action", - "prepublish": "yarn build publish" + "prepublish": "yarn build --target publish" } } diff --git a/scripts/build.js b/scripts/build.js index a71ba6048e..392d1cfde6 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,15 +1,22 @@ const fs = require("fs-extra") const path = require("path") const execa = require("execa") +const minimist = require("minimist") const stdio = ["ignore", "inherit", "ignore"] const opts = { stdio } -const packageName = process.argv[2] +const { + _: [packageName], + target +} = minimist(process.argv.slice(2)) // build to publish needs to do more things so it's slower // for the CI run and local testing this is not necessary -const isPublish = process.argv[3] === "publish" +const isPublish = target === "publish" + +// for running tests in CI we need CJS only +const isTest = target === "test" const tempMove = name => fs.move(`dist/${name}`, `temp/${name}`) const moveTemp = name => fs.move(`temp/${name}`, `dist/${name}`) @@ -28,7 +35,11 @@ const run = async () => { await tempMove(`${packageName}.esm.production.min.js.map`) } - await execa("tsdx", ["build", "--name", packageName, "--format", "esm,cjs,umd"], opts) + await execa( + "tsdx", + ["build", "--name", packageName, "--format", isTest ? "cjs" : "esm,cjs,umd"], + opts + ) if (isPublish) { // move ESM bundles back to dist folder and remove temp diff --git a/yarn.lock b/yarn.lock index 053b0dae72..56c4721156 100644 --- a/yarn.lock +++ b/yarn.lock @@ -905,9 +905,9 @@ regenerator-runtime "^0.13.4" "@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740" - integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA== + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== dependencies: regenerator-runtime "^0.13.4" @@ -1230,10 +1230,10 @@ slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/core@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.2.tgz#6d669385c3fda0e2271464de890da4122e61548e" - integrity sha512-x0v0LVlEslGYGYk4StT90NUp7vbFBrh0K7KDyAg3hMhG0drrxOIQHsY05uC7XVlKHXFgGI+HdnU35qewMZOLFQ== +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== dependencies: "@jest/console" "^26.6.2" "@jest/reporters" "^26.6.2" @@ -1246,14 +1246,14 @@ exit "^0.1.2" graceful-fs "^4.2.4" jest-changed-files "^26.6.2" - jest-config "^26.6.2" + jest-config "^26.6.3" jest-haste-map "^26.6.2" jest-message-util "^26.6.2" jest-regex-util "^26.0.0" jest-resolve "^26.6.2" - jest-resolve-dependencies "^26.6.2" - jest-runner "^26.6.2" - jest-runtime "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" jest-snapshot "^26.6.2" jest-util "^26.6.2" jest-validate "^26.6.2" @@ -1437,16 +1437,16 @@ jest-runner "^25.5.4" jest-runtime "^25.5.4" -"@jest/test-sequencer@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.2.tgz#4f9a705d0368f61a820bd9a281c8ce83a1facaf3" - integrity sha512-iHiEXLMP69Ohe6kFMOVz6geADRxwK+OkLGg0VIGfZrUdkJGiCpghkMb2946FLh7jvzOwwZGyQoMi+kaHiOdM5g== +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== dependencies: "@jest/test-result" "^26.6.2" graceful-fs "^4.2.4" jest-haste-map "^26.6.2" - jest-runner "^26.6.2" - jest-runtime "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" "@jest/transform@^25.5.1": version "25.5.1" @@ -1491,6 +1491,15 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + "@jest/types@^25.5.0": version "25.5.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.5.0.tgz#4d6a4793f7b9599fc3680877b856a97dbccf2a9d" @@ -1611,6 +1620,11 @@ estree-walker "^1.0.1" picomatch "^2.2.2" +"@sheerun/mutationobserver-shim@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25" + integrity sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw== + "@sinonjs/commons@^1.7.0": version "1.8.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" @@ -1625,6 +1639,42 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@testing-library/dom@^6.15.0": + version "6.16.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-6.16.0.tgz#04ada27ed74ad4c0f0d984a1245bb29b1fd90ba9" + integrity sha512-lBD88ssxqEfz0wFL6MeUyyWZfV/2cjEZZV3YRpb2IoJRej/4f1jB0TzqIOznTpfR1r34CNesrubxwIlAQ8zgPA== + dependencies: + "@babel/runtime" "^7.8.4" + "@sheerun/mutationobserver-shim" "^0.3.2" + "@types/testing-library__dom" "^6.12.1" + aria-query "^4.0.2" + dom-accessibility-api "^0.3.0" + pretty-format "^25.1.0" + wait-for-expect "^3.0.2" + +"@testing-library/jest-dom@^5.1.1": + version "5.11.5" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.11.5.tgz#44010f37f4b1e15f9d433963b515db0b05182fc8" + integrity sha512-XI+ClHR864i6p2kRCEyhvpVejuer+ObVUF4cjCvRSF88eOMIfqw7RoS9+qoRhyigGswMfT64L6Nt0Ufotxbwtg== + dependencies: + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^4.2.2" + chalk "^3.0.0" + css "^3.0.0" + css.escape "^1.5.1" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@^9.4.0": + version "9.5.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-9.5.0.tgz#71531655a7890b61e77a1b39452fbedf0472ca5e" + integrity sha512-di1b+D0p+rfeboHO5W7gTVeZDIK5+maEgstrZbWZSSvxDyfDRkkyBE1AJR5Psd6doNldluXlCWqXriUfqu/9Qg== + dependencies: + "@babel/runtime" "^7.8.4" + "@testing-library/dom" "^6.15.0" + "@types/testing-library__react" "^9.1.2" + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.12.tgz#4d8e9e51eb265552a7e4f1ff2219ab6133bdfb2d" @@ -1702,7 +1752,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@26.x", "@types/jest@^26.0.15": +"@types/jest@*", "@types/jest@26.x", "@types/jest@^26.0.15": version "26.0.15" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" integrity sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog== @@ -1751,6 +1801,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.3.tgz#a6e252973214079155f749e8bef99cc80af182fa" integrity sha512-8Jduo8wvvwDzEVJCOvS/G6sgilOLvvhn1eMmK3TW8/T217O7u1jdrK6ImKLv80tVryaPSVeKu6sjDEiFjd4/eg== +"@types/node@^10.0.0": + version "10.17.44" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.44.tgz#3945e6b702cb6403f22b779c8ea9e5c3f44ead40" + integrity sha512-vHPAyBX1ffLcy4fQHmDyIUMUb42gHZjPHU66nhvbMzAWJqHnySGZ6STwN3rwrnSd1FHB0DI/RWgGELgKSYRDmw== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -1771,6 +1826,26 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== +"@types/prop-types@*", "@types/prop-types@^15.5.2": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + +"@types/react-dom@*", "@types/react-dom@^16.0.5": + version "16.9.9" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.9.tgz#d2d0a6f720a0206369ccbefff752ba37b9583136" + integrity sha512-jE16FNWO3Logq/Lf+yvEAjKzhpST/Eac8EMd1i4dgZdMczfgqC8EjpxwNgEe3SExHYLliabXDh9DEhhqnlXJhg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^16.8.24": + version "16.9.55" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.55.tgz#47078587f5bfe028a23b6b46c7b94ac0d436acff" + integrity sha512-6KLe6lkILeRwyyy7yG9rULKJ0sXplUsl98MGoCfpteXf9sPWFWWMknDcsvubcpaTdBuxtsLF6HDUwdApZL/xIg== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -1793,11 +1868,41 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/testing-library__dom@*", "@types/testing-library__dom@^6.12.1": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz#1aede831cb4ed4a398448df5a2c54b54a365644e" + integrity sha512-sMl7OSv0AvMOqn1UJ6j1unPMIHRXen0Ita1ujnMX912rrOcawe4f7wu0Zt9GIQhBhJvH2BaibqFgQ3lP+Pj2hA== + dependencies: + pretty-format "^24.3.0" + +"@types/testing-library__jest-dom@^5.9.1": + version "5.9.5" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz#5bf25c91ad2d7b38f264b12275e5c92a66d849b0" + integrity sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ== + dependencies: + "@types/jest" "*" + +"@types/testing-library__react@^9.1.2": + version "9.1.3" + resolved "https://registry.yarnpkg.com/@types/testing-library__react/-/testing-library__react-9.1.3.tgz#35eca61cc6ea923543796f16034882a1603d7302" + integrity sha512-iCdNPKU3IsYwRK9JieSYAiX0+aYDXOGAmrC/3/M7AqqSDKnWWVv07X+Zk1uFSL7cMTUYzv4lQRfohucEocn5/w== + dependencies: + "@types/react-dom" "*" + "@types/testing-library__dom" "*" + pretty-format "^25.1.0" + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== +"@types/yargs@^13.0.0": + version "13.0.11" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.11.tgz#def2f0c93e4bdf2c61d7e34899b17e34be28d3b1" + integrity sha512-NRqD6T4gktUrDi1o1wLH3EKC1o2caCr7/wR87ODcbVITQF106OM3sFN92ysZ++wqelOd1CTzatnOBRDYYG6wGQ== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^15.0.0": version "15.0.9" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.9.tgz#524cd7998fe810cdb02f26101b699cccd156ff19" @@ -2186,7 +2291,7 @@ ansi-regex@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.1.0: +ansi-regex@^4.0.0, ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== @@ -2243,7 +2348,7 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -aria-query@^4.2.2: +aria-query@^4.0.2, aria-query@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== @@ -2575,10 +2680,10 @@ babel-jest@^25.5.1: graceful-fs "^4.2.4" slash "^3.0.0" -babel-jest@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.2.tgz#ca84659b1683e6e5bf16609bc88f3f2f086fe443" - integrity sha512-pysyz/mZ7T5sozKnvSa1n7QEf22W9yc+dUmn2zNuQTN0saG51q8A/8k9wbED9X4YNxmwjuhIwf4JRXXQGzui3Q== +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== dependencies: "@jest/transform" "^26.6.2" "@jest/types" "^26.6.2" @@ -3860,6 +3965,20 @@ css-selector-tokenizer@^0.7.0: cssesc "^3.0.0" fastparse "^1.1.2" +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + +css@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" + integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== + dependencies: + inherits "^2.0.4" + source-map "^0.6.1" + source-map-resolve "^0.6.0" + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -3928,6 +4047,11 @@ cssstyle@^2.0.0, cssstyle@^2.2.0: dependencies: cssom "~0.3.6" +csstype@^3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.4.tgz#b156d7be03b84ff425c9a0a4b1e5f4da9c5ca888" + integrity sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA== + csv-generate@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/csv-generate/-/csv-generate-3.2.4.tgz#440dab9177339ee0676c9e5c16f50e2b3463c019" @@ -4184,6 +4308,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.3.0.tgz#511e5993dd673b97c87ea47dba0e3892f7e0c983" + integrity sha512-PzwHEmsRP3IGY4gv/Ug+rMeaTIyTJvadCb+ujYXYeIylbHJezIyNToe8KfEgHTCEYyC+/bUghYOGg8yMGlZ6vA== + domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -6076,12 +6205,12 @@ jest-cli@^25.5.4: realpath-native "^2.0.0" yargs "^15.3.1" -jest-cli@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.2.tgz#6f42b002c2f0a0902eed7fa55fafdb528b39e764" - integrity sha512-5SBxa0bXc43fTHgxMfonDFDWTmQTiC6RSS4GpKhVekWkwpaeMHWt/FvGIy5GlTHMbCpzULWV++N3v93OdlFfQA== +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== dependencies: - "@jest/core" "^26.6.2" + "@jest/core" "^26.6.3" "@jest/test-result" "^26.6.2" "@jest/types" "^26.6.2" chalk "^4.0.0" @@ -6089,7 +6218,7 @@ jest-cli@^26.6.2: graceful-fs "^4.2.4" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^26.6.2" + jest-config "^26.6.3" jest-util "^26.6.2" jest-validate "^26.6.2" prompts "^2.0.1" @@ -6120,15 +6249,15 @@ jest-config@^25.5.4: pretty-format "^25.5.0" realpath-native "^2.0.0" -jest-config@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.2.tgz#f5d2667e71b5b5fbb910cf1898446f3d48a6a0ab" - integrity sha512-0ApZqPd+L/BUWvNj1GHcptb5jwF23lo+BskjgJV/Blht1hgpu6eIwaYRgHPrS6I6HrxwRfJvlGbzoZZVb3VHTA== +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.6.2" + "@jest/test-sequencer" "^26.6.3" "@jest/types" "^26.6.2" - babel-jest "^26.6.2" + babel-jest "^26.6.3" chalk "^4.0.0" deepmerge "^4.2.2" glob "^7.1.1" @@ -6136,7 +6265,7 @@ jest-config@^26.6.2: jest-environment-jsdom "^26.6.2" jest-environment-node "^26.6.2" jest-get-type "^26.3.0" - jest-jasmine2 "^26.6.2" + jest-jasmine2 "^26.6.3" jest-regex-util "^26.0.0" jest-resolve "^26.6.2" jest-util "^26.6.2" @@ -6323,10 +6452,10 @@ jest-jasmine2@^25.5.4: pretty-format "^25.5.0" throat "^5.0.0" -jest-jasmine2@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.2.tgz#81bc3eabc367aa65cb9e63ec7129f8831cc345fc" - integrity sha512-Om6q632kogggOBGjSr34jErXGOQy0+IkxouGUbyzB0lQmufu8nm1AcxLIKpB/FN36I43f2T3YajeNlxwJZ94PQ== +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== dependencies: "@babel/traverse" "^7.1.0" "@jest/environment" "^26.6.2" @@ -6341,7 +6470,7 @@ jest-jasmine2@^26.6.2: jest-each "^26.6.2" jest-matcher-utils "^26.6.2" jest-message-util "^26.6.2" - jest-runtime "^26.6.2" + jest-runtime "^26.6.3" jest-snapshot "^26.6.2" jest-util "^26.6.2" pretty-format "^26.6.2" @@ -6412,6 +6541,11 @@ jest-message-util@^26.6.2: slash "^3.0.0" stack-utils "^2.0.2" +jest-mock-console@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/jest-mock-console/-/jest-mock-console-1.0.1.tgz#07978047735a782d0d4172d1afcabd82f6de9b08" + integrity sha512-Bn+Of/cvz9LOEEeEg5IX5Lsf8D2BscXa3Zl5+vSVJl37yiT8gMAPPKfE09jJOwwu1zbagL11QTrH+L/Gn8udOg== + jest-mock@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.5.0.tgz#a91a54dabd14e37ecd61665d6b6e06360a55387a" @@ -6451,10 +6585,10 @@ jest-resolve-dependencies@^25.5.4: jest-regex-util "^25.2.6" jest-snapshot "^25.5.1" -jest-resolve-dependencies@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.2.tgz#82b5456bfa9544bb6e376397c8de334d5deba0ce" - integrity sha512-lXXQqBLlKlnOPyCfJZnrYydd7lZzWux9sMwKJxOmjsuVmoSlnmTOJ8kW1FYxotTyMzqoNtBuSF6qE+iXuAr6qQ== +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== dependencies: "@jest/types" "^26.6.2" jest-regex-util "^26.0.0" @@ -6514,10 +6648,10 @@ jest-runner@^25.5.4: source-map-support "^0.5.6" throat "^5.0.0" -jest-runner@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.2.tgz#eaa7a2ef38e043054ab8c84c045a09873893d364" - integrity sha512-OsWTIGx/MHSuPqjYwap1LAxT0qvlqmwTYSFOwc+G14AtyZlL7ngrrDes7moLRqFkDVpCHL2RT0i317jogyw81Q== +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== dependencies: "@jest/console" "^26.6.2" "@jest/environment" "^26.6.2" @@ -6528,13 +6662,13 @@ jest-runner@^26.6.2: emittery "^0.7.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-config "^26.6.2" + jest-config "^26.6.3" jest-docblock "^26.0.0" jest-haste-map "^26.6.2" jest-leak-detector "^26.6.2" jest-message-util "^26.6.2" jest-resolve "^26.6.2" - jest-runtime "^26.6.2" + jest-runtime "^26.6.3" jest-util "^26.6.2" jest-worker "^26.6.2" source-map-support "^0.5.6" @@ -6572,10 +6706,10 @@ jest-runtime@^25.5.4: strip-bom "^4.0.0" yargs "^15.3.1" -jest-runtime@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.2.tgz#c0989ea9c55f0cab0ab5a403b7a0af56c72f3c9a" - integrity sha512-VEjfoim4tkvq8Gh8z7wMXlKva3DnIlgvmGR1AajiRK1nEHuXtuaR17jnVYOi+wW0i1dS3NH4jVdUQl08GodgZQ== +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== dependencies: "@jest/console" "^26.6.2" "@jest/environment" "^26.6.2" @@ -6592,7 +6726,7 @@ jest-runtime@^26.6.2: exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-config "^26.6.2" + jest-config "^26.6.3" jest-haste-map "^26.6.2" jest-message-util "^26.6.2" jest-mock "^26.6.2" @@ -6783,13 +6917,13 @@ jest@^25.3.0: jest-cli "^25.5.4" jest@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.2.tgz#d116f55438129360f523c22b5cf010f88740272d" - integrity sha512-lL0hW7mh/2hhQmpo/1fDWQji/BUB3Xcxxj7r0fAOa3t56OAnwbE0HEl2bZ7XjAwV5TXOt8UpCgaa/WBJBB0CYw== + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== dependencies: - "@jest/core" "^26.6.2" + "@jest/core" "^26.6.3" import-local "^3.0.2" - jest-cli "^26.6.2" + jest-cli "^26.6.3" jpjs@^1.2.1: version "1.2.1" @@ -7277,7 +7411,7 @@ lolex@^5.0.0: dependencies: "@sinonjs/commons" "^1.7.0" -loose-envify@^1.0.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -7558,6 +7692,11 @@ mkdirp@1.0.4, mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mobx-react-lite@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.0.1.tgz#417f54a819d1e3e00c073077f29373399c95b005" + integrity sha512-Ue8uGgT5iOjMyNf5ptoFW7BTvyLIwggzIkoFpwORrqf73TPqu47iLpz/DNvaba3v40kSsEpp050qYroMNuA1xw== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -8557,7 +8696,17 @@ prettier@^2.0.5: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== -pretty-format@^25.2.1, pretty-format@^25.5.0: +pretty-format@^24.3.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== + dependencies: + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + +pretty-format@^25.1.0, pretty-format@^25.2.1, pretty-format@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a" integrity sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ== @@ -8632,7 +8781,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -8751,7 +8900,17 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -react-is@^16.12.0, react-is@^16.8.1: +react-dom@^16.9.0: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" + integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.19.1" + +react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -8761,6 +8920,15 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +react@^16.9.0: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" + integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -9289,6 +9457,14 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" +scheduler@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" + integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -10609,6 +10785,11 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" +wait-for-expect@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/wait-for-expect/-/wait-for-expect-3.0.2.tgz#d2f14b2f7b778c9b82144109c8fa89ceaadaa463" + integrity sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag== + walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"