diff --git a/.browserlistrc b/.browserlistrc deleted file mode 100644 index 915b6805..00000000 --- a/.browserlistrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "targets": { - "chrome": "58", - "ie": "9" - } -} diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 8062647f..00000000 --- a/.npmignore +++ /dev/null @@ -1,29 +0,0 @@ -/node_modules/ - -src/ -test/ -coverage/ - -.vscode -.rpt2_cache -.rts2_cache_* -.circleci -.editorconfig -.prettierignore -.prettierrc -.browserlistrc -.travis.yml -.size-limit.json - -empty.js -build-rollup.js -jest.config.js -jest.setup.js -tsconfig.json -tsconfig.build.json -tslint.json -renovate.json - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/.size-limit.json b/.size-limit.json index adc6a83a..2781d558 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -1,13 +1,13 @@ [ { "path": "dist/mobxreactlite.umd.production.min.js", - "limit": "1.7 KB", + "limit": "1.8 KB", "webpack": false, "running": false }, { "path": "dist/mobxreactlite.cjs.production.min.js", - "limit": "1.7 KB", + "limit": "1.8 KB", "webpack": false, "running": false } diff --git a/README.md b/README.md index 66d7c4fd..cb34ee01 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ -# mobx-react-lite +# mobx-react-lite -[![CircleCI](https://circleci.com/gh/mobxjs/mobx-react-lite.svg?style=svg)](https://circleci.com/gh/mobxjs/mobx-react-lite)[![Coverage Status](https://coveralls.io/repos/github/mobxjs/mobx-react-lite/badge.svg)](https://coveralls.io/github/mobxjs/mobx-react-lite) +[![CircleCI](https://circleci.com/gh/mobxjs/mobx-react-lite.svg?style=svg)](https://circleci.com/gh/mobxjs/mobx-react-lite)[![Coverage Status](https://coveralls.io/repos/github/mobxjs/mobx-react-lite/badge.svg)](https://coveralls.io/github/mobxjs/mobx-react-lite)[![NPM downloads](https://img.shields.io/npm/dm/mobx-react-lite.svg?style=flat)](https://npmjs.com/package/mobx-react-lite)[![Minzipped size](https://img.shields.io/bundlephobia/minzip/mobx-react-lite.svg)](https://bundlephobia.com/result?p=mobx-react-lite) + +[![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.svg?v=101)](https://github.com/ellerbrock/typescript-badges/)[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![Join the chat at https://gitter.im/mobxjs/mobx](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/mobxjs/mobx?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -This is a next iteration of [mobx-react](https://github.com/mobxjs/mobx-react) coming from introducing React hooks which simplifies a lot of internal workings of this package. +[![NPM](https://nodei.co/npm/mobx-react-lite.png)](https://www.npmjs.com/package/mobx-react-lite) **You need React version 16.8.0 and above** -Class based components **are not supported** except using `` directly in class `render` method. If you want to transition existing projects from classes to hooks (as most of us do), you can use this package alongside the [mobx-react](https://github.com/mobxjs/mobx-react) just fine. The only conflict point is about the `observer` HOC. Subscribe [to this issue](https://github.com/mobxjs/mobx-react/issues/640) for a proper migration guide. +This is a lighter version of [mobx-react](https://github.com/mobxjs/mobx-react) which supports React **functional components only** and as such makes the library slightly faster and smaller (_only 1.5kB gzipped_). In fact `mobx-react@6` has this library as a dependency and builds on top of it. -[![NPM](https://nodei.co/npm/mobx-react-lite.png)](https://www.npmjs.com/package/mobx-react-lite) +The library does not include any Provider/inject utilities as they can be fully replaced with [React Context](https://mobx-react.js.org/recipes-context). Check out [the migration guide](https://mobx-react.js.org/recipes-migration). -Project is written in TypeScript and provides type safety out of the box. No Flow Type support is planned at this moment, but feel free to contribute. +Class based components **are not supported** except using `` directly in class `render` method. If you want to transition existing projects from classes to hooks, use [mobx-react 6+](https://github.com/mobxjs/mobx-react). + +See more at [the libraries overview](https://mobx-react.js.org/libraries). ## User Guide 👉 https://mobx-react.js.org @@ -20,18 +24,23 @@ The site contains various examples and recipes for using MobX in React world. Fe ## API reference ⚒ -> **`{renderFn}`** _([user guide](https://mobx-react.js.org/observer-component))_ +### **`{renderFn}`** _([user guide](https://mobx-react.js.org/observer-component))_ + +Is a React component, which applies observer to an anonymous region in your component. -> **`observer

(baseComponent: FunctionComponent

, options?: IObserverOptions): FunctionComponent

`** _([user guide](https://mobx-react.js.org/observer-hoc))_ +### **`observer

(baseComponent: FunctionComponent

, options?: IObserverOptions): FunctionComponent

`** _([user guide](https://mobx-react.js.org/observer-hoc))_ ```ts interface IObserverOptions { - // Pass true to use React.forwardRef over the inner component. It's false by the default. + // Pass true to wrap the inner component with React.forwardRef. + // It's false by the default. forwardRef?: boolean } ``` -> **`useObserver(fn: () => T, baseComponentName = "observed", options?: IUseObserverOptions): T`** _([user guide](https://mobx-react.js.org/observer-hook))_ +The observer converts a component into a reactive component, which tracks which observables are used automatically and re-renders the component when one of these values changes. + +### **`useObserver(fn: () => T, baseComponentName = "observed", options?: IUseObserverOptions): T`** _([user guide](https://mobx-react.js.org/observer-hook))_ ```ts interface IUseObserverOptions { @@ -40,167 +49,43 @@ interface IUseObserverOptions { } ``` -**`useLocalStore(initializer: () => T, source?: S): T`** _([user guide](https://mobx-react.js.org/state-local))_ - -**`useAsObservableSource(source: T): T`** _([user guide](https://mobx-react.js.org/state-outsourcing))_ - -## React Strict mode ☄ +It allows you to use an observer like behaviour, but still allowing you to optimize the component in any way you want (e.g. using memo with a custom areEqual, using forwardRef, etc.) and to declare exactly the part that is observed (the render phase). -Feel free to try out `mobx-react-lite@next` which is based on latest 1.x, but contains experimental support for handling Concurrent mode in React properly. +### **`useLocalStore(initializer: () => T, source?: S): T`** _([user guide](https://mobx-react.js.org/state-local))_ -## Optimize rendering +Local observable state can be introduced by using the useLocalStore hook, that runs its initializer function once to create an observable store and keeps it around for a lifetime of a component. -[Check out the elaborate explanation](https://github.com/mobxjs/mobx-react-lite/issues/153#issuecomment-490511464). +### **`useAsObservableSource(source: T): T`** _([user guide](https://mobx-react.js.org/state-outsourcing))_ -If this is something that concerns you, we have prepared files you can simply import to configure MobX to use React batched updates depending on your platform. - -**React DOM:** +The useAsObservableSource hook can be used to turn any set of values into an observable object that has a stable reference (the same object is returned every time from the hook). -> import 'mobx-react-lite/optimizeForReactDom' +## Observer batching -**React Native:** +[Check out the elaborate explanation](https://github.com/mobxjs/mobx-react/pull/787#issuecomment-573599793). -> import 'mobx-react-lite/optimizeForReactNative' +In short without observer batching the React doesn't guarantee the order component rendering in some cases. We highly recommend that you configure batching to avoid these random surprises. Import one of these before any React rendering is happening, typically `index.js/ts`. For Jest tests you can utilize [setupFilesAfterEnv](https://jestjs.io/docs/en/configuration#setupfilesafterenv-array). -### Custom batched updates - -Above imports are for a convenience. If you for some reason have customized version of batched updates, you can do the following instead. - -```js -import { optimizeScheduler } from "mobx-react-lite" -optimizeScheduler(customBatchedUpdates) -``` - -## Deprecation notice ⚠ - -Following utilities are still available in the package, but they are deprecated and will be removed in the next major version (2.x). As such, they are not mentioned in the user guide and it's not recommend to continue using these. - ---- - -### `useObservable(initialValue: T): T` - -> **Use the `useLocalStore` instead** ([user guide](https://mobx-react.js.org/state-local)) - -React hook that allows creating observable object within a component body and keeps track of it over renders. Gets all the benefits from [observable objects](https://mobx.js.org/refguide/object.html) including computed properties and methods. You can also use arrays, Map and Set. - -Warning: With current implementation you also need to wrap your component to `observer`. It's also possible to have `useObserver` only in case you are not expecting rerender of the whole component. - -```tsx -import { useObservable, useObserver } from "mobx-react-lite" - -const TodoList = () => { - const todos = useObservable(new Map()) - const todoRef = React.useRef() - const addTodo = React.useCallback(() => { - todos.set(todoRef.current.value, false) - todoRef.current.value = "" - }, []) - const toggleTodo = React.useCallback((todo: string) => { - todos.set(todo, !todos.get(todo)) - }, []) - - return useObserver(() => ( -

- {Array.from(todos).map(([todo, done]) => ( -
toggleTodo(todo)} key={todo}> - {todo} - {done ? " ✔" : " ⏲"} -
- ))} - - -
- )) -} -``` - -#### Lazy initialization - -Lazy initialization (similar to `React.useState`) is not available. In most cases your observable state should be a plain object which is cheap to create. With `useObserver` the component won't even rerender and state won't be recreated. In case you really want a more complex state or you need to use `observer`, it's very simple to use MobX directly. - -```tsx -import { observer } from "mobx-react-lite" -import { observable } from "mobx" -import { useState } from "react" - -const WithComplexState = observer(() => { - const [complexState] = useState(() => observable(new HeavyState())) - if (complexState.loading) { - return - } - return
{complexState.heavyName}
-}) -``` - -[![Edit TodoList](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/jzj48v2xry?module=%2Fsrc%2FTodoList.tsx) - -Note that if you want to track a single scalar value (string, number, boolean), you would need [a boxed value](https://mobx.js.org/refguide/boxed.html) which is not recognized by `useObservable`. However, we recommend to just `useState` instead which gives you almost same result (with slightly different API). - -### `useComputed(func: () => T, inputs: ReadonlyArray = []): T` - -> **Use the `useLocalStore` instead** ([user guide](https://mobx-react.js.org/state-local)) - -Another React hook that simplifies computational logic. It's just a tiny wrapper around [MobX computed](https://mobx.js.org/refguide/computed-decorator.html#-computed-expression-as-function) function that runs computation whenever observable values change. In conjuction with `observer` the component will rerender based on such a change. - -```tsx -const Calculator = observer(({ hasExploded }: { hasExploded: boolean }) => { - const inputRef = React.useRef() - const inputs = useObservable([1, 3, 5]) - const result = useComputed( - () => (hasExploded ? "💣" : inputs.reduce(multiply, 1) * Number(!hasExploded)), - [hasExploded] - ) - - return ( -
- - -
- {inputs.join(" * ")} = {result} -
-
- ) -}) -``` - -Notice that since the computation depends on non-observable value, it has to be passed as a second argument to `useComputed`. There is [React `useMemo`](https://reactjs.org/docs/hooks-reference.html#usememo) behind the scenes and all rules applies here as well except you don't need to specify dependency on observable values. +**React DOM:** -[![Edit Calculator](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/jzj48v2xry?module=%2Fsrc%2FCalculator.tsx) +> import 'mobx-react-lite/batchingForReactDom' -### `useDisposable(disposerGenerator: () => D, inputs: ReadonlyArray = []): D` +**React Native:** -> **Use the `React.useEffect` instead** ([user guide](https://mobx-react.js.org/recipes-effects)) +> import 'mobx-react-lite/batchingForReactNative' -The disposable is any kind of function that returns another function to be called on a component unmount to clean up used resources. Use MobX related functions like [`reaction`](https://mobx.js.org/refguide/reaction.html), [`autorun`](https://mobx.js.org/refguide/autorun.html), [`when`](https://mobx.js.org/refguide/when.html), [`observe`](https://mobx.js.org/refguide/observe.html), or anything else that returns a disposer. -Returns the generated disposer for early disposal. +### Opt-out -Example (TypeScript): +To opt-out from batching in some specific cases, simply import the following to silence the warning. -```typescript -import { reaction } from "mobx" -import { observer, useComputed, useDisposable } from "mobx-react-lite" +> import 'mobx-react-lite/batchingOptOut' -const Name = observer((props: { firstName: string; lastName: string }) => { - const fullName = useComputed(() => `${props.firstName} ${props.lastName}`, [ - props.firstName, - props.lastName - ]) +### Custom batched updates - // when the name changes then send this info to the server - useDisposable(() => - reaction( - () => fullName, - () => { - // send this to some server - } - ) - ) +Above imports are for a convenience to utilize standard versions of batching. If you for some reason have customized version of batched updates, you can do the following instead. - // render phase - return `Your full name is ${props.firstName} ${props.lastName}` -}) +```js +import { observerBatching } from "mobx-react-lite" +observerBatching(customBatchedUpdates) ``` diff --git a/optimizeForReactDom.js b/batchingForReactDom.js similarity index 100% rename from optimizeForReactDom.js rename to batchingForReactDom.js diff --git a/optimizeForReactNative.js b/batchingForReactNative.js similarity index 100% rename from optimizeForReactNative.js rename to batchingForReactNative.js diff --git a/batchingOptOut.js b/batchingOptOut.js new file mode 100644 index 00000000..414694e6 --- /dev/null +++ b/batchingOptOut.js @@ -0,0 +1 @@ +require("./dist").observerBatchingOptOut() diff --git a/empty.js b/empty.js deleted file mode 100644 index 13eb3bc4..00000000 --- a/empty.js +++ /dev/null @@ -1,3 +0,0 @@ -// These functions can be stubbed out in specific environments -export const unstable_batchedUpdates = undefined -export const findDOMNode = undefined diff --git a/jest.setup.js b/jest.setup.js index 5180185b..17068791 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,2 +1,4 @@ // should be to be in each test file to load typings properly require("@testing-library/jest-dom/extend-expect") + +global.__DEV__ = false diff --git a/package.json b/package.json index 8c365158..65424a7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mobx-react-lite", - "version": "1.5.2", + "version": "2.0.0-alpha.5", "description": "Lightweight React bindings for MobX based on React 16.8 and Hooks", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -14,6 +14,10 @@ "type": "git", "url": "https://github.com/mobxjs/mobx-react-lite.git" }, + "files": [ + "dist/*", + "batching*" + ], "scripts": { "prettier": "prettier --write \"./{src,test}/*.{js,ts,tsx}\"", "lint": "eslint . --ext .js,.ts,.tsx", @@ -24,7 +28,6 @@ "coverage": "jest --coverage", "prebuild": "rimraf dist", "build": "yarn bundle", - "postbuild": "shx cp dist/mobxreactlite.umd.production.min.js dist/index.min.js", "bundle": "tsdx build --name mobxReactLite --format=cjs,esm,umd --tsconfig tsconfig.build.json", "prepublishOnly": "yarn build", "dedup": "npx yarn-deduplicate -s fewer yarn.lock" diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..404c55f1 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1 @@ +declare const __DEV__: boolean diff --git a/src/index.ts b/src/index.ts index 65077b62..40ec7596 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,5 @@ import "./assertEnvironment" -export { useObservable } from "./useObservable" -export { useComputed } from "./useComputed" -export { useDisposable } from "./useDisposable" export { isUsingStaticRendering, useStaticRendering } from "./staticRendering" export { observer, IObserverOptions } from "./observer" export { useObserver, ForceUpdateHook, IUseObserverOptions } from "./useObserver" @@ -10,4 +7,4 @@ export { Observer } from "./ObserverComponent" export { useForceUpdate } from "./utils" export { useAsObservableSource } from "./useAsObservableSource" export { useLocalStore } from "./useLocalStore" -export { optimizeScheduler } from "./optimizeScheduler" +export { observerBatching, observerBatchingOptOut, isObserverBatched } from "./observerBatching" diff --git a/src/observer.ts b/src/observer.ts index 0d6bf963..250cf3e8 100644 --- a/src/observer.ts +++ b/src/observer.ts @@ -13,10 +13,29 @@ export function observer

( ): React.MemoExoticComponent< React.ForwardRefExoticComponent & React.RefAttributes> > + export function observer

( baseComponent: React.FunctionComponent

, options?: IObserverOptions ): React.FunctionComponent

+ +export function observer< + C extends React.FunctionComponent | React.RefForwardingComponent, + Options extends IObserverOptions +>( + baseComponent: C, + options?: Options +): Options extends { forwardRef: true } + ? C extends React.RefForwardingComponent + ? C & + React.MemoExoticComponent< + React.ForwardRefExoticComponent< + React.PropsWithoutRef

& React.RefAttributes + > + > + : never /* forwardRef set for a non forwarding component */ + : C & React.FunctionComponent + // n.b. base case is not used for actual typings or exported in the typing files export function observer

( baseComponent: React.RefForwardingComponent, diff --git a/src/optimizeScheduler.ts b/src/observerBatching.ts similarity index 54% rename from src/optimizeScheduler.ts rename to src/observerBatching.ts index 03c29722..6ece3ce2 100644 --- a/src/optimizeScheduler.ts +++ b/src/observerBatching.ts @@ -1,17 +1,23 @@ import { configure } from "mobx" -export interface IBatchedUpdates { +interface IBatchedUpdates { batchedUpdates(callback: (a: A, b: B) => any, a: A, b: B): void batchedUpdates(callback: (a: A) => any, a: A): void batchedUpdates(callback: () => any): void } -export const optimizeScheduler = (reactionScheduler: IBatchedUpdates) => { +let observerBatchingConfigured = false + +export const observerBatching = (reactionScheduler?: IBatchedUpdates) => { if (typeof reactionScheduler === "function") { configure({ reactionScheduler }) } + observerBatchingConfigured = true } -export const deoptimizeScheduler = () => { +export const observerBatchingOptOut = () => { configure({ reactionScheduler: undefined }) + observerBatchingConfigured = true } + +export const isObserverBatched = () => observerBatchingConfigured diff --git a/src/printDebugValue.ts b/src/printDebugValue.ts index ef0e4f83..8ef487fd 100644 --- a/src/printDebugValue.ts +++ b/src/printDebugValue.ts @@ -1,8 +1,5 @@ import { getDependencyTree, Reaction } from "mobx" -export function printDebugValue(v: React.MutableRefObject) { - if (!v.current) { - return "" - } - return getDependencyTree(v.current) +export function printDebugValue(v: Reaction) { + return getDependencyTree(v) } diff --git a/src/reactionCleanupTracking.ts b/src/reactionCleanupTracking.ts new file mode 100644 index 00000000..3de158f8 --- /dev/null +++ b/src/reactionCleanupTracking.ts @@ -0,0 +1,125 @@ +import { Reaction } from "mobx" + +export interface IReactionTracking { + /** The Reaction created during first render, which may be leaked */ + reaction: Reaction + /** + * The time (in ticks) at which point we should dispose of the reaction + * if this component hasn't yet been fully mounted. + */ + cleanAt: number + + /** + * Whether the component has yet completed mounting (for us, whether + * its useEffect has run) + */ + mounted?: boolean + + /** + * Whether the observables that the component is tracking changed between + * the first render and the first useEffect. + */ + changedBeforeMount?: boolean +} + +export function createTrackingData(reaction: Reaction) { + const trackingData: IReactionTracking = { + cleanAt: Date.now() + CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS, + reaction + } + return trackingData +} + +/** + * The minimum time before we'll clean up a Reaction created in a render + * for a component that hasn't managed to run its effects. This needs to + * be big enough to ensure that a component won't turn up and have its + * effects run without being re-rendered. + */ +export const CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS = 10_000 + +/** + * The frequency with which we'll check for leaked reactions. + */ +export const CLEANUP_TIMER_LOOP_MILLIS = 10_000 + +/** + * Reactions created by components that have yet to be fully mounted. + */ +const uncommittedReactionRefs: Set> = new Set() + +/** + * Latest 'uncommitted reactions' cleanup timer handle. + */ +let reactionCleanupHandle: ReturnType | undefined + +function ensureCleanupTimerRunning() { + if (reactionCleanupHandle === undefined) { + reactionCleanupHandle = setTimeout(cleanUncommittedReactions, CLEANUP_TIMER_LOOP_MILLIS) + } +} + +export function scheduleCleanupOfReactionIfLeaked( + ref: React.MutableRefObject +) { + uncommittedReactionRefs.add(ref) + + ensureCleanupTimerRunning() +} + +export function recordReactionAsCommitted( + reactionRef: React.MutableRefObject +) { + uncommittedReactionRefs.delete(reactionRef) +} + +/** + * Run by the cleanup timer to dispose any outstanding reactions + */ +function cleanUncommittedReactions() { + reactionCleanupHandle = undefined + + // Loop through all the candidate leaked reactions; those older + // than CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS get tidied. + + const now = Date.now() + for (const ref of uncommittedReactionRefs) { + const tracking = ref.current + if (tracking) { + if (now >= tracking.cleanAt) { + // It's time to tidy up this leaked reaction. + tracking.reaction.dispose() + ref.current = null + uncommittedReactionRefs.delete(ref) + } + } + } + + if (uncommittedReactionRefs.size > 0) { + // We've just finished a round of cleanups but there are still + // some leak candidates outstanding. + ensureCleanupTimerRunning() + } +} + +/* istanbul ignore next */ +/** + * Only to be used by test functions; do not export outside of mobx-react-lite + */ +export function forceCleanupTimerToRunNowForTests() { + // This allows us to control the execution of the cleanup timer + // to force it to run at awkward times in unit tests. + if (reactionCleanupHandle) { + clearTimeout(reactionCleanupHandle) + cleanUncommittedReactions() + } +} + +/* istanbul ignore next */ +export function resetCleanupScheduleForTests() { + if (reactionCleanupHandle) { + clearTimeout(reactionCleanupHandle) + reactionCleanupHandle = undefined + } + uncommittedReactionRefs.clear() +} diff --git a/src/useComputed.ts b/src/useComputed.ts deleted file mode 100644 index 83e0f4fc..00000000 --- a/src/useComputed.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as mobx from "mobx" -import { useMemo } from "react" - -let warned = false - -export function useComputed(func: () => T, inputs: ReadonlyArray = []): T { - if (process.env.NODE_ENV !== "production" && !warned) { - warned = true - // tslint:disable-next-line: no-console - console.warn( - "[mobx-react-lite] useComputed has been deprecated. Use useLocalStore instead." - ) - } - const computed = useMemo(() => mobx.computed(func), inputs) - return computed.get() -} diff --git a/src/useDisposable.ts b/src/useDisposable.ts deleted file mode 100644 index eb437137..00000000 --- a/src/useDisposable.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { useEffect, useRef } from "react" - -type TDisposable = () => void - -const doNothingDisposer = () => { - // empty -} - -let warned = false - -/** - * Adds an observable effect (reaction, autorun, or anything else that returns a disposer) that will be registered upon component creation and disposed upon unmounting. - * Returns the generated disposer for early disposal. - * - * @export - * @template D - * @param {() => D} disposerGenerator A function that returns the disposer of the wanted effect. - * @param {ReadonlyArray} [inputs=[]] If you want the effect to be automatically re-created when some variable(s) are changed then pass them in this array. - * @returns {D} - */ -export function useDisposable( - disposerGenerator: () => D, - inputs: ReadonlyArray = [] -): D { - if (process.env.NODE_ENV !== "production" && !warned) { - warned = true - // tslint:disable-next-line: no-console - console.warn( - "[mobx-react-lite] useDisposable has been deprecated. Use React.useEffect instead." - ) - } - const disposerRef = useRef(null) - const earlyDisposedRef = useRef(false) - - useEffect(() => { - return lazyCreateDisposer(false) - }, inputs) - - function lazyCreateDisposer(earlyDisposal: boolean) { - // ensure that we won't create a new disposer if it was early disposed - if (earlyDisposedRef.current) { - return doNothingDisposer - } - - if (!disposerRef.current) { - const newDisposer = disposerGenerator() - - if (typeof newDisposer !== "function") { - const error = new Error("generated disposer must be a function") - if (process.env.NODE_ENV !== "production") { - throw error - } else { - // tslint:disable-next-line:no-console - console.error(error) - return doNothingDisposer - } - } - - disposerRef.current = newDisposer - } - return () => { - if (disposerRef.current) { - disposerRef.current() - disposerRef.current = null - } - if (earlyDisposal) { - earlyDisposedRef.current = true - } - } - } - - return lazyCreateDisposer(true) as D -} diff --git a/src/useObservable.ts b/src/useObservable.ts deleted file mode 100644 index 1c5208ec..00000000 --- a/src/useObservable.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { observable } from "mobx" -import { useRef } from "react" - -type SupportedValues = object | Map | any[] - -let warned = false - -export function useObservable(initialValue: T): T { - if (process.env.NODE_ENV !== "production" && !warned) { - warned = true - // tslint:disable-next-line: no-console - console.warn( - "[mobx-react-lite] useObservable has been deprecated. Use useLocalStore instead." - ) - } - - const observableRef = useRef(null) - if (!observableRef.current) { - observableRef.current = observable(initialValue) - } - - return observableRef.current -} diff --git a/src/useObserver.ts b/src/useObserver.ts index f21580ea..d1cda43f 100644 --- a/src/useObserver.ts +++ b/src/useObserver.ts @@ -1,9 +1,16 @@ import { Reaction } from "mobx" -import { useDebugValue, useRef } from "react" +import React from "react" +import { isObserverBatched } from "./observerBatching" import { printDebugValue } from "./printDebugValue" +import { + createTrackingData, + IReactionTracking, + recordReactionAsCommitted, + scheduleCleanupOfReactionIfLeaked +} from "./reactionCleanupTracking" import { isUsingStaticRendering } from "./staticRendering" -import { useForceUpdate, useUnmount } from "./utils" +import { useForceUpdate } from "./utils" export type ForceUpdateHook = () => () => void @@ -13,6 +20,12 @@ export interface IUseObserverOptions { const EMPTY_OBJECT = {} +function observerComponentNameFor(baseComponentName: string) { + return `observer${baseComponentName}` +} + +let warnedAboutBatching = false + export function useObserver( fn: () => T, baseComponentName: string = "observed", @@ -22,35 +35,90 @@ export function useObserver( return fn() } + if (__DEV__ && !warnedAboutBatching && !isObserverBatched()) { + console.warn( + `[MobX] You haven't configured observer batching which might result in unexpected behavior in some cases. See more at https://github.com/mobxjs/mobx-react-lite/#observer-batching` + ) + warnedAboutBatching = true + } + const wantedForceUpdateHook = options.useForceUpdate || useForceUpdate const forceUpdate = wantedForceUpdateHook() - const reaction = useRef(null) - if (!reaction.current) { - reaction.current = new Reaction(`observer(${baseComponentName})`, () => { - forceUpdate() + // StrictMode/ConcurrentMode/Suspense may mean that our component is + // rendered and abandoned multiple times, so we need to track leaked + // Reactions. + const reactionTrackingRef = React.useRef(null) + + if (!reactionTrackingRef.current) { + // First render for this component (or first time since a previous + // reaction from an abandoned render was disposed). + + const newReaction = new Reaction(observerComponentNameFor(baseComponentName), () => { + // Observable has changed, meaning we want to re-render + // BUT if we're a component that hasn't yet got to the useEffect() + // stage, we might be a component that _started_ to render, but + // got dropped, and we don't want to make state changes then. + // (It triggers warnings in StrictMode, for a start.) + if (trackingData.mounted) { + // We have reached useEffect(), so we're mounted, and can trigger an update + forceUpdate() + } else { + // We haven't yet reached useEffect(), so we'll need to trigger a re-render + // when (and if) useEffect() arrives. The easiest way to do that is just to + // drop our current reaction and allow useEffect() to recreate it. + newReaction.dispose() + reactionTrackingRef.current = null + } }) - } - const dispose = () => { - if (reaction.current && !reaction.current.isDisposed) { - reaction.current.dispose() - reaction.current = null - } + const trackingData = createTrackingData(newReaction) + reactionTrackingRef.current = trackingData + scheduleCleanupOfReactionIfLeaked(reactionTrackingRef) } - useDebugValue(reaction, printDebugValue) + const { reaction } = reactionTrackingRef.current! + React.useDebugValue(reaction, printDebugValue) - useUnmount(() => { - dispose() - }) + React.useEffect(() => { + // Called on first mount only + recordReactionAsCommitted(reactionTrackingRef) + + if (reactionTrackingRef.current) { + // Great. We've already got our reaction from our render; + // all we need to do is to record that it's now mounted, + // to allow future observable changes to trigger re-renders + reactionTrackingRef.current.mounted = true + } else { + // The reaction we set up in our render has been disposed. + // This is either due to bad timings of renderings, e.g. our + // component was paused for a _very_ long time, and our + // reaction got cleaned up, or we got a observable change + // between render and useEffect + + // Re-create the reaction + reactionTrackingRef.current = { + reaction: new Reaction(observerComponentNameFor(baseComponentName), () => { + // We've definitely already been mounted at this point + forceUpdate() + }), + cleanAt: Infinity + } + forceUpdate() + } + + return () => { + reactionTrackingRef.current!.reaction.dispose() + reactionTrackingRef.current = null + } + }, []) // render the original component, but have the // reaction track the observables, so that rendering // can be invalidated (see above) once a dependency changes let rendering!: T let exception - reaction.current.track(() => { + reaction.track(() => { try { rendering = fn() } catch (e) { @@ -58,7 +126,6 @@ export function useObserver( } }) if (exception) { - dispose() throw exception // re-throw any exceptions catched during rendering } return rendering diff --git a/test/observer.test.tsx b/test/observer.test.tsx index 7a0e8125..402b8fc3 100644 --- a/test/observer.test.tsx +++ b/test/observer.test.tsx @@ -581,12 +581,12 @@ it("should hoist known statics only", () => { const wrapped = observer(MyHipsterComponent) expect(wrapped.displayName).toBe("MyHipsterComponent") - expect((wrapped as any).randomStaticThing).toEqual(3) - expect((wrapped as any).defaultProps).toEqual({ x: 3 }) - expect((wrapped as any).propTypes).toEqual({ x: isNumber }) - expect((wrapped as any).type).toBeInstanceOf(Function) // And not "Nope!"; this is the wrapped component, the property is introduced by memo - expect((wrapped as any).compare).toBe(null) // another memo field - expect((wrapped as any).render).toBe(undefined) + expect(wrapped.randomStaticThing).toEqual(3) + expect(wrapped.defaultProps).toEqual({ x: 3 }) + expect(wrapped.propTypes).toEqual({ x: isNumber }) + expect(wrapped.type).toBeInstanceOf(Function) // And not "Nope!"; this is the wrapped component, the property is introduced by memo + expect(wrapped.compare).toBe(null) // another memo field + expect(wrapped.render).toBe(undefined) }) it("should have the correct displayName", () => { @@ -658,6 +658,110 @@ test("parent / childs render in the right order", done => { done() }) +it("should have overload for props with children", () => { + interface IProps { + value: string; + } + const TestComponent = observer(({ value, children }) => { + return null + }) + + render() + + // this test has no `expect` calls as it verifies whether such component compiles or not +}) + +it("should have overload for empty options", () => { + // empty options are not really making sense now, but we shouldn't rely on `forwardRef` + // being specified in case other options are added in the future + + interface IProps { + value: string; + } + const TestComponent = observer(({ value, children }) => { + return null + }, {}) + + render() + + // this test has no `expect` calls as it verifies whether such component compiles or not +}) + +it("should have overload for props with children when forwardRef", () => { + interface IMethods { + focus(): void + } + + interface IProps { + value: string; + } + const TestComponent = observer(({ value, children }, ref) => { + return null + }, { forwardRef: true }) + + render() + + // this test has no `expect` calls as it verifies whether such component compiles or not +}) + +it("should preserve generic parameters", () => { + interface IColor { + name: string; + css: string; + } + + interface ITestComponentProps { + value: T; + callback: (value: T) => void; + } + const TestComponent = observer((props: ITestComponentProps) => { + return null + }) + + function callbackString(value: string) { + return; + } + function callbackColor(value: IColor) { + return; + } + + render() + render() + + // this test has no `expect` calls as it verifies whether such component compiles or not +}) + +it("should preserve generic parameters when forwardRef", () => { + interface IMethods { + focus(): void + } + + interface IColor { + name: string + css: string + } + + interface ITestComponentProps { + value: T + callback: (value: T) => void + } + const TestComponent = observer((props: ITestComponentProps, ref: React.Ref) => { + return null + }, { forwardRef: true }) + + function callbackString(value: string) { + return; + } + function callbackColor(value: IColor) { + return; + } + + render() + render() + + // this test has no `expect` calls as it verifies whether such component compiles or not +}) + // describe("206 - @observer should produce usefull errors if it throws", () => { // const data = mobx.observable({ x: 1 }) // let renderCount = 0 diff --git a/test/printDebugValue.test.ts b/test/printDebugValue.test.ts index 85f3f86b..69754e51 100644 --- a/test/printDebugValue.test.ts +++ b/test/printDebugValue.test.ts @@ -17,13 +17,11 @@ test("printDebugValue", () => { } }) - const refLike = { - current: (disposer as any)[$mobx] - } + const value = (disposer as any)[$mobx] - expect(printDebugValue(refLike)).toMatchSnapshot() + expect(printDebugValue(value)).toMatchSnapshot() disposer() - expect(printDebugValue(refLike)).toMatchSnapshot() + expect(printDebugValue(value)).toMatchSnapshot() }) diff --git a/test/strictAndConcurrentMode.test.tsx b/test/strictAndConcurrentMode.test.tsx new file mode 100644 index 00000000..e5fbe6ec --- /dev/null +++ b/test/strictAndConcurrentMode.test.tsx @@ -0,0 +1,268 @@ +import { act, cleanup, render } from "@testing-library/react" +import mockConsole from "jest-mock-console" +import * as mobx from "mobx" +import * as React from "react" +import ReactDOM from "react-dom" + +import { useObserver } from "../src" +import { + CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS, + CLEANUP_TIMER_LOOP_MILLIS, + forceCleanupTimerToRunNowForTests, + resetCleanupScheduleForTests, +} from "../src/reactionCleanupTracking" + +afterEach(cleanup) + +test("uncommitted observing components should not attempt state changes", () => { + const store = mobx.observable({ count: 0 }) + + const TestComponent = () => useObserver(() =>

) + + // Render our observing component wrapped in StrictMode + const rendering = render( + + + + ) + + // That will have caused our component to have been rendered + // more than once, but when we unmount it'll only unmount once. + rendering.unmount() + + // Trigger a change to the observable. If the reactions were + // not disposed correctly, we'll see some console errors from + // React StrictMode because we're calling state mutators to + // trigger an update. + const restoreConsole = mockConsole() + try { + act(() => { + store.count++ + }) + + // Check to see if any console errors were reported. + // tslint:disable-next-line: no-console + expect(console.error).not.toHaveBeenCalled() + } finally { + restoreConsole() + } +}) + +const strictModeValues = [true, false] +strictModeValues.forEach(strictMode => { + const modeName = strictMode ? "StrictMode" : "non-StrictMode" + + test(`observable changes before first commit are not lost (${modeName})`, () => { + const store = mobx.observable({ value: "initial" }) + + const TestComponent = () => useObserver(() =>
{store.value}
) + + // Render our observing component wrapped in StrictMode, but using + // raw ReactDOM.render (not react-testing-library render) because we + // don't want the useEffect calls to have run just yet... + const rootNode = document.createElement("div") + + let elem = + if (strictMode) { + elem = {elem} + } + + ReactDOM.render(elem, rootNode) + + // Change our observable. This is happening between the initial render of + // our component and its initial commit, so it isn't fully mounted yet. + // We want to ensure that the change isn't lost. + store.value = "changed" + + act(() => { + // no-op + }) + + expect(rootNode.textContent).toBe("changed") + }) +}) + +test("uncommitted components should not leak observations", async () => { + resetCleanupScheduleForTests() + + // Unfortunately, Jest fake timers don't mock out Date.now, so we fake + // that out in parallel to Jest useFakeTimers + let fakeNow = Date.now() + jest.useFakeTimers() + jest.spyOn(Date, "now").mockImplementation(() => fakeNow) + + const store = mobx.observable({ count1: 0, count2: 0 }) + + // Track whether counts are observed + let count1IsObserved = false + let count2IsObserved = false + mobx.onBecomeObserved(store, "count1", () => (count1IsObserved = true)) + mobx.onBecomeUnobserved(store, "count1", () => (count1IsObserved = false)) + mobx.onBecomeObserved(store, "count2", () => (count2IsObserved = true)) + mobx.onBecomeUnobserved(store, "count2", () => (count2IsObserved = false)) + + const TestComponent1 = () => useObserver(() =>
{store.count1}
) + const TestComponent2 = () => useObserver(() =>
{store.count2}
) + + // Render, then remove only #2 + const rendering = render( + + + + + ) + rendering.rerender( + + + + ) + + // Allow any reaction-disposal cleanup timers to run + const skip = Math.max(CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS, CLEANUP_TIMER_LOOP_MILLIS) + fakeNow += skip + jest.advanceTimersByTime(skip) + + // count1 should still be being observed by Component1, + // but count2 should have had its reaction cleaned up. + expect(count1IsObserved).toBeTruthy() + expect(count2IsObserved).toBeFalsy() +}) + +test("cleanup timer should not clean up recently-pended reactions", () => { + // If we're not careful with timings, it's possible to get the + // following scenario: + // 1. Component instance A is being created; it renders, we put its reaction R1 into the cleanup list + // 2. Strict/Concurrent mode causes that render to be thrown away + // 3. Component instance A is being created; it renders, we put its reaction R2 into the cleanup list + // 4. The MobX reaction timer from 5 seconds ago kicks in and cleans up all reactions from uncommitted + // components, including R1 and R2 + // 5. The commit phase runs for component A, but reaction R2 has already been disposed. Game over. + + // This unit test attempts to replicate that scenario: + resetCleanupScheduleForTests() + + // Unfortunately, Jest fake timers don't mock out Date.now, so we fake + // that out in parallel to Jest useFakeTimers + const fakeNow = Date.now() + jest.useFakeTimers() + jest.spyOn(Date, "now").mockImplementation(() => fakeNow) + + const store = mobx.observable({ count: 0 }) + + // Track whether the count is observed + let countIsObserved = false + mobx.onBecomeObserved(store, "count", () => (countIsObserved = true)) + mobx.onBecomeUnobserved(store, "count", () => (countIsObserved = false)) + + const TestComponent1 = () => useObserver(() =>
{store.count}
) + + // We're going to render directly using ReactDOM, not react-testing-library, because we want + // to be able to get in and do nasty things before everything (including useEffects) have completed, + // and react-testing-library waits for all that, using act(). + + const rootNode = document.createElement("div") + ReactDOM.render( + // We use StrictMode here, but it would be helpful to switch this to use real + // concurrent mode: we don't have a true async render right now so this test + // isn't as thorough as it could be. + + + , + rootNode + ) + + // We need to trigger our cleanup timer to run. We can't do this simply + // by running all jest's faked timers as that would allow the scheduled + // `useEffect` calls to run, and we want to simulate our cleanup timer + // getting in between those stages. + + // We force our cleanup loop to run even though enough time hasn't _really_ + // elapsed. In theory, it won't do anything because not enough time has + // elapsed since the reactions were queued, and so they won't be disposed. + forceCleanupTimerToRunNowForTests() + + // Advance time enough to allow any timer-queued effects to run + jest.advanceTimersByTime(500) + + // Now allow the useEffect calls to run to completion. + act(() => { + // no-op, but triggers effect flushing + }) + + // count should still be observed + expect(countIsObserved).toBeTruthy() +}) + +test("component should recreate reaction if necessary", () => { + // There _may_ be very strange cases where the reaction gets tidied up + // but is actually still needed. This _really_ shouldn't happen. + // e.g. if we're using Suspense and the component starts to render, + // but then gets paused for 60 seconds, and then comes back to life. + // With the implementation of React at the time of writing this, React + // will actually fully re-render that component (discarding previous + // hook slots) before going ahead with a commit, but it's unwise + // to depend on such an implementation detail. So we must cope with + // the component having had its reaction tidied and still going on to + // be committed. In that case we recreate the reaction and force + // an update. + + // This unit test attempts to replicate that scenario: + + resetCleanupScheduleForTests() + + // Unfortunately, Jest fake timers don't mock out Date.now, so we fake + // that out in parallel to Jest useFakeTimers + let fakeNow = Date.now() + jest.useFakeTimers() + jest.spyOn(Date, "now").mockImplementation(() => fakeNow) + + const store = mobx.observable({ count: 0 }) + + // Track whether the count is observed + let countIsObserved = false + mobx.onBecomeObserved(store, "count", () => (countIsObserved = true)) + mobx.onBecomeUnobserved(store, "count", () => (countIsObserved = false)) + + const TestComponent1 = () => useObserver(() =>
{store.count}
) + + // We're going to render directly using ReactDOM, not react-testing-library, because we want + // to be able to get in and do nasty things before everything (including useEffects) have completed, + // and react-testing-library waits for all that, using act(). + const rootNode = document.createElement("div") + ReactDOM.render( + + + , + rootNode + ) + + // We need to trigger our cleanup timer to run. We don't want + // to allow Jest's effects to run, however: we want to simulate the + // case where the component is rendered, then the reaction gets cleaned up, + // and _then_ the component commits. + + // Force everything to be disposed. + const skip = Math.max(CLEANUP_LEAKED_REACTIONS_AFTER_MILLIS, CLEANUP_TIMER_LOOP_MILLIS) + fakeNow += skip + forceCleanupTimerToRunNowForTests() + + // The reaction should have been cleaned up. + expect(countIsObserved).toBeFalsy() + + // Whilst nobody's looking, change the observable value + store.count = 42 + + // Now allow the useEffect calls to run to completion, + // re-awakening the component. + jest.advanceTimersByTime(500) + act(() => { + // no-op, but triggers effect flushing + }) + + // count should be observed once more. + expect(countIsObserved).toBeTruthy() + // and the component should have rendered enough to + // show the latest value, which was set whilst it + // wasn't even looking. + expect(rootNode.textContent).toContain("42") +}) diff --git a/test/useComputed.test.tsx b/test/useComputed.test.tsx deleted file mode 100644 index b09d919e..00000000 --- a/test/useComputed.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { observable } from "mobx" -import * as React from "react" -import { act, cleanup, render } from "@testing-library/react" - -import { observer, useComputed } from "../src" - -afterEach(cleanup) - -describe("is used to rerender based on a computed value change", () => { - it("keeps track of observable values", () => { - const TestComponent = observer((props: any) => { - const value = useComputed(() => props.store.x + 5 * props.store.y) - return
{value}
- }) - const store = observable({ x: 5, y: 1 }) - const { container } = render() - const div = container.querySelector("div")! - expect(div.textContent).toBe("10") - act(() => { - store.y = 2 - }) - expect(div.textContent).toBe("15") - act(() => { - store.x = 10 - }) - expect(div.textContent).toBe("20") - }) - - it("allows non-observables to be used if specified as inputs", () => { - const TestComponent = observer((props: any) => { - const value = useComputed(() => props.store.x + 5 * props.y, [props.y]) - return
{value}
- }) - const store = observable({ x: 5 }) - const { container, rerender } = render() - const div = container.querySelector("div")! - expect(div.textContent).toBe("10") - rerender() - expect(div.textContent).toBe("15") - act(() => { - store.x = 10 - }) - expect(div.textContent).toBe("20") - }) -}) diff --git a/test/useDisposable.test.tsx b/test/useDisposable.test.tsx deleted file mode 100644 index 420c895d..00000000 --- a/test/useDisposable.test.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import mockConsole from "jest-mock-console" -import { observable, reaction } from "mobx" -import * as React from "react" -import { act, cleanup, render } from "@testing-library/react" - -import { observer, useDisposable } from "../src" -import { productionMode } from "./utils" - -afterEach(cleanup) - -test("reactions run and dispose properly", async () => { - let reactions1Created = 0 - let reactions2Created = 0 - let reactions1 = 0 - let reactions2 = 0 - let renders = 0 - let reaction1DisposerCalls = 0 - let reaction2DisposerCalls = 0 - - const store = observable({ - prop1: 0, - prop2: 0 - }) - - let firstReaction!: () => void - - const Component = observer((props: { store: typeof store; a?: number }) => { - firstReaction = useDisposable(() => { - reactions1Created++ - const disposer = reaction( - () => props.store.prop1, - () => { - reactions1++ - } - ) - - return () => { - reaction1DisposerCalls++ - disposer() - } - }, [props.a]) - - useDisposable(() => { - reactions2Created++ - const disposer = reaction( - () => props.store.prop2, - () => { - reactions2++ - } - ) - - return () => { - reaction2DisposerCalls++ - disposer() - } - }, [props.a]) - - renders++ - return ( -
- {props.store.prop1} {props.store.prop2} {props.a} -
- ) - }) - - const { rerender, unmount } = render() - expect(reactions1Created).toBe(1) - expect(reaction1DisposerCalls).toBe(0) - expect(reactions2Created).toBe(1) - expect(reaction2DisposerCalls).toBe(0) - expect(renders).toBe(1) - expect(reactions1).toBe(0) - expect(reactions2).toBe(0) - - act(() => { - store.prop1 = 1 - }) - expect(reactions1Created).toBe(1) - expect(reaction1DisposerCalls).toBe(0) - expect(reactions2Created).toBe(1) - expect(reaction2DisposerCalls).toBe(0) - expect(renders).toBe(2) - expect(reactions1).toBe(1) - expect(reactions2).toBe(0) - - act(() => { - store.prop2 = 1 - }) - expect(reactions1Created).toBe(1) - expect(reaction1DisposerCalls).toBe(0) - expect(reactions2Created).toBe(1) - expect(reaction2DisposerCalls).toBe(0) - expect(renders).toBe(3) - expect(reactions1).toBe(1) - expect(reactions2).toBe(1) - - // early dispose one of them, it shouldn't be re-created when one of the dependent inputs change - firstReaction() - expect(reactions1Created).toBe(1) - expect(reaction1DisposerCalls).toBe(1) // early disposal - expect(reactions2Created).toBe(1) - expect(reaction2DisposerCalls).toBe(0) // this one is not early disposed - - rerender() - expect(reactions1Created).toBe(1) // depends on a, but was early disposed, so it should not increment - expect(reaction1DisposerCalls).toBe(1) - expect(reactions2Created).toBe(2) // depends on a, so it gets re-created - expect(reaction2DisposerCalls).toBe(1) - expect(renders).toBe(4) - expect(reactions1).toBe(1) - expect(reactions2).toBe(1) - - unmount() - expect(reactions1Created).toBe(1) - expect(reaction1DisposerCalls).toBe(1) - expect(reactions2Created).toBe(2) - expect(reaction2DisposerCalls).toBe(2) - expect(renders).toBe(4) - expect(reactions1).toBe(1) - expect(reactions2).toBe(1) -}) - -test("disposer needs to be a function or else throws/console.error", async () => { - const error = "generated disposer must be a function" - - const Component1 = observer(() => { - useDisposable(() => { - return undefined as any - }) - return
test
- }) - - const Component2 = observer(() => { - useDisposable(() => { - return "string" as any - }) - return
test
- }) - - const restoreConsole = mockConsole() - // tslint:disable-next-line:no-console - const mockConsoleError = console.error as jest.Mock<{}> - - expect(() => { - render() - }).toThrow(error) - - expect(() => { - render() - }).toThrow(error) - - productionMode(() => { - mockConsoleError.mockClear() - render() - expect(mockConsoleError.mock.calls[0][0].message).toEqual(error) - - mockConsoleError.mockClear() - render() - expect(mockConsoleError.mock.calls[0][0].message).toEqual(error) - }) - restoreConsole() -}) diff --git a/test/useObservable.test.tsx b/test/useObservable.test.tsx deleted file mode 100644 index d5231ea8..00000000 --- a/test/useObservable.test.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import * as mobx from "mobx" -import * as React from "react" -import { cleanup, fireEvent, render } from "@testing-library/react" - -import { observer, useObservable } from "../src" - -afterEach(cleanup) - -describe("is used to keep observable within component body", () => { - it("value can be changed over renders", () => { - const TestComponent = () => { - const obs = useObservable({ - x: 1, - y: 2 - }) - return ( -
(obs.x += 1)}> - {obs.x}-{obs.y} -
- ) - } - const { container, rerender } = render() - const div = container.querySelector("div")! - expect(div.textContent).toBe("1-2") - fireEvent.click(div) - // observer not used, need to render from outside - rerender() - expect(div.textContent).toBe("2-2") - }) - - it("works with observer as well", () => { - const spyObservable = jest.spyOn(mobx, "observable") - - let renderCount = 0 - - const TestComponent = observer(() => { - renderCount++ - - const obs = useObservable({ - x: 1, - y: 2 - }) - return ( -
(obs.x += 1)}> - {obs.x}-{obs.y} -
- ) - }) - const { container } = render() - const div = container.querySelector("div")! - expect(div.textContent).toBe("1-2") - fireEvent.click(div) - expect(div.textContent).toBe("2-2") - fireEvent.click(div) - expect(div.textContent).toBe("3-2") - - // though render 3 times, mobx.observable only called once - expect(renderCount).toBe(3) - expect(spyObservable.mock.calls.length).toBe(1) - - spyObservable.mockRestore() - }) - - it("actions can be used", () => { - const TestComponent = observer(() => { - const obs = useObservable({ - x: 1, - y: 2, - inc() { - obs.x += 1 - } - }) - return ( -
- {obs.x}-{obs.y} -
- ) - }) - const { container } = render() - const div = container.querySelector("div")! - expect(div.textContent).toBe("1-2") - fireEvent.click(div) - expect(div.textContent).toBe("2-2") - }) - - it("computed properties works as well", () => { - const TestComponent = observer(() => { - const obs = useObservable({ - x: 1, - y: 2, - get z() { - return obs.x + obs.y - } - }) - return
(obs.x += 1)}>{obs.z}
- }) - const { container } = render() - const div = container.querySelector("div")! - expect(div.textContent).toBe("3") - fireEvent.click(div) - expect(div.textContent).toBe("4") - }) - - it("Map can used instead of object", () => { - const TestComponent = observer(() => { - const map = useObservable(new Map([["initial", 10]])) - return ( -
map.set("later", 20)}> - {Array.from(map).map(([key, value]) => ( -
- {key} - {value} -
- ))} -
- ) - }) - const { container } = render() - const div = container.querySelector("div")! - expect(div.textContent).toBe("initial - 10") - fireEvent.click(div) - expect(div.textContent).toBe("initial - 10later - 20") - }) -}) diff --git a/test/utils.ts b/test/utils.ts index dabe4a13..4bb601fc 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,16 +1,5 @@ import { configure } from "mobx" -export function productionMode(fn: () => void) { - const oldNodeEnv = process.env.NODE_ENV - process.env.NODE_ENV = "production" - - try { - fn() - } finally { - process.env.NODE_ENV = oldNodeEnv - } -} - export function resetMobx(): void { configure({ enforceActions: "never" }) } diff --git a/yarn.lock b/yarn.lock index fdf2506f..f2e72dd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,7 +25,7 @@ invariant "^2.2.4" semver "^5.5.0" -"@babel/core@7.8.4", "@babel/core@^7.4.4": +"@babel/core@7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.4.tgz#d496799e5c12195b3602d0fddd77294e3e38e80e" integrity sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA== @@ -47,17 +47,17 @@ source-map "^0.5.0" "@babel/core@^7.1.0": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b" - integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA== + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a" + integrity sha512-OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.3.4" - "@babel/helpers" "^7.2.0" - "@babel/parser" "^7.3.4" - "@babel/template" "^7.2.2" - "@babel/traverse" "^7.3.4" - "@babel/types" "^7.3.4" + "@babel/generator" "^7.4.4" + "@babel/helpers" "^7.4.4" + "@babel/parser" "^7.4.5" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.5" + "@babel/types" "^7.4.4" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -66,17 +66,48 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0", "@babel/generator@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e" - integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg== +"@babel/core@^7.4.4": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.0.tgz#9b00f73554edd67bebc86df8303ef678be3d7b48" + integrity sha512-FuRhDRtsd6IptKpHXAa+4WPZYY2ZzgowkbLBecEDDSje1X/apG7jQM33or3NdOmjXBKWGOg4JmSiRfUfuTtHXw== dependencies: - "@babel/types" "^7.3.4" + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.0" + "@babel/helpers" "^7.6.0" + "@babel/parser" "^7.6.0" + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.0" + "@babel/types" "^7.6.0" + convert-source-map "^1.1.0" + debug "^4.1.0" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.0.0", "@babel/generator@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041" + integrity sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ== + dependencies: + "@babel/types" "^7.4.4" jsesc "^2.5.1" lodash "^4.17.11" source-map "^0.5.0" trim-right "^1.0.1" +"@babel/generator@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.0.tgz#e2c21efbfd3293ad819a2359b448f002bfdfda56" + integrity sha512-Ms8Mo7YBdMMn1BYuNtKuP/z0TgEIhbcyB8HVR6PPNYp4P61lMsABiS4A3VG1qznjXVCf3r+fVHhm4efTYVsySA== + dependencies: + "@babel/types" "^7.6.0" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + trim-right "^1.0.1" + "@babel/generator@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.4.tgz#35bbc74486956fe4251829f9f6c48330e8d0985e" @@ -277,12 +308,12 @@ "@babel/template" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-split-export-declaration@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813" - integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag== +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== dependencies: - "@babel/types" "^7.0.0" + "@babel/types" "^7.4.4" "@babel/helper-split-export-declaration@^7.8.3": version "7.8.3" @@ -301,14 +332,23 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helpers@^7.2.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9" - integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA== +"@babel/helpers@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5" + integrity sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A== dependencies: - "@babel/template" "^7.1.2" - "@babel/traverse" "^7.1.5" - "@babel/types" "^7.3.0" + "@babel/template" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/helpers@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.0.tgz#21961d16c6a3c3ab597325c34c465c0887d31c6e" + integrity sha512-W9kao7OBleOjfXtFGgArGRX6eCP0UEcA2ZWEWNkJdRZnHhW4eEbeswbG3EwaRsnQUAEGWYgMq1HsIXuNNNy2eQ== + dependencies: + "@babel/template" "^7.6.0" + "@babel/traverse" "^7.6.0" + "@babel/types" "^7.6.0" "@babel/helpers@^7.8.4": version "7.8.4" @@ -337,11 +377,21 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.2", "@babel/parser@^7.3.4": +"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.2": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c" integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ== +"@babel/parser@^7.4.4", "@babel/parser@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" + integrity sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew== + +"@babel/parser@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b" + integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ== + "@babel/parser@^7.8.3", "@babel/parser@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.4.tgz#d1dbe64691d60358a974295fa53da074dd2ce8e8" @@ -823,7 +873,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": +"@babel/template@^7.0.0", "@babel/template@^7.1.0": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== @@ -832,6 +882,24 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" +"@babel/template@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/template@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" + integrity sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.0" + "@babel/template@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8" @@ -841,7 +909,7 @@ "@babel/parser" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06" integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ== @@ -856,6 +924,36 @@ globals "^11.1.0" lodash "^4.17.11" +"@babel/traverse@^7.4.4", "@babel/traverse@^7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216" + integrity sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/generator" "^7.4.4" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.4.5" + "@babel/types" "^7.4.4" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.11" + +"@babel/traverse@^7.6.0": + version "7.6.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.0.tgz#389391d510f79be7ce2ddd6717be66d3fed4b516" + integrity sha512-93t52SaOBgml/xY74lsmt7xOR4ufYvhb5c5qiM6lu4J/dWGMAfAh6eKw4PjLes6DI6nQgearoxnFJk60YchpvQ== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.6.0" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.6.0" + "@babel/types" "^7.6.0" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.4.tgz#f0845822365f9d5b0e312ed3959d3f827f869e3c" @@ -871,7 +969,7 @@ globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4": +"@babel/types@^7.0.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ== @@ -880,6 +978,24 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" +"@babel/types@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0" + integrity sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ== + dependencies: + esutils "^2.0.2" + lodash "^4.17.11" + to-fast-properties "^2.0.0" + +"@babel/types@^7.6.0": + version "7.6.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648" + integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@babel/types@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c" @@ -1245,7 +1361,7 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== -"@types/estree@*", "@types/estree@0.0.39": +"@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== @@ -1306,7 +1422,7 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@*", "@types/node@12.7.12": +"@types/node@*", "@types/node@12.7.12", "@types/node@^12.7.2": version "12.7.12" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.12.tgz#7c6c571cc2f3f3ac4a59a5f2bd48f5bdbc8653cc" integrity sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ== @@ -1658,7 +1774,7 @@ acorn@^6.0.1, acorn@^6.0.7, acorn@^6.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== -acorn@^7.1.0: +acorn@^7.0.0, acorn@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== @@ -2670,7 +2786,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.18.0, commander@^2.20.0: +commander@^2.11.0, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== @@ -3096,9 +3212,9 @@ cssstyle@^1.0.0: cssom "0.3.x" csstype@^2.2.0: - version "2.6.5" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.5.tgz#1cd1dff742ebf4d7c991470ae71e12bb6751e034" - integrity sha512-JsTaiksRsel5n7XwqPAfB0l3TFKdpjW/kgAELf9vrb5adGA7UCPLajKK5s3nFrcFm3Rkyp/Qkgl73ENc1UY3cA== + version "2.6.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.6.tgz#c34f8226a94bbb10c32cc0d714afdf942291fc41" + integrity sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg== cyclist@~0.2.2: version "0.2.2" @@ -3421,11 +3537,6 @@ emoji-regex@^7.0.1, emoji-regex@^7.0.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -4146,15 +4257,6 @@ find-cache-dir@^2.1.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.0.0.tgz#cd4b7dd97b7185b7e17dbfe2d6e4115ee3eeb8fc" - integrity sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.0" - pkg-dir "^4.1.0" - find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" @@ -4243,7 +4345,7 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" -fs-extra@8.1.0, fs-extra@^8.0.1: +fs-extra@^8.0.1: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== @@ -4282,7 +4384,7 @@ fsevents@^1.2.7: nan "^2.9.2" node-pre-gyp "^0.10.0" -function-bind@^1.1.1: +function-bind@^1.0.2, function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== @@ -4986,11 +5088,6 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - is-generator-fn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e" @@ -5578,7 +5675,7 @@ jest-watcher@^24.3.0, jest-watcher@^24.9.0: jest-util "^24.9.0" string-length "^2.0.0" -jest-worker@^24.6.0, jest-worker@^24.9.0: +jest-worker@^24.0.0, jest-worker@^24.6.0, jest-worker@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== @@ -6029,13 +6126,6 @@ make-dir@^2.0.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801" - integrity sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw== - dependencies: - semver "^6.0.0" - make-error@1.x: version "1.3.5" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" @@ -7051,7 +7141,7 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0, pkg-dir@^4.2.0: +pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -7944,7 +8034,7 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@1.12.0, resolve@1.x, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.3.2: +resolve@1.x, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.3.2: version "1.12.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== @@ -8054,13 +8144,6 @@ rollup-plugin-typescript2@^0.25.3: rollup-pluginutils "2.8.1" tslib "1.10.0" -rollup-pluginutils@2.8.1, rollup-pluginutils@^2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97" - integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg== - dependencies: - estree-walker "^0.6.1" - rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.8.2: version "2.8.2" resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" @@ -8068,6 +8151,13 @@ rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.8.2: dependencies: estree-walker "^0.6.1" +rollup-pluginutils@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97" + integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg== + dependencies: + estree-walker "^0.6.1" + rollup@^1.27.8: version "1.31.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.31.1.tgz#4170d6f87148d46e5fbe29b493f8f3ea3453c96f" @@ -8209,7 +8299,7 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: +semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -8233,6 +8323,11 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" +serialize-javascript@^1.6.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb" + integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A== + serialize-javascript@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" @@ -8458,6 +8553,14 @@ source-map-support@^0.5.6, source-map-support@~0.5.12: buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@~0.5.10: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" @@ -8866,6 +8969,15 @@ terser-webpack-plugin@^1.4.1: webpack-sources "^1.4.0" worker-farm "^1.7.0" +terser@^3.14.1: + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== + dependencies: + commander "^2.19.0" + source-map "~0.6.1" + source-map-support "~0.5.10" + terser@^4.1.2: version "4.2.0" resolved "https://registry.yarnpkg.com/terser/-/terser-4.2.0.tgz#4b1b5f4424b426a7a47e80d6aae45e0d7979aef0" @@ -9145,7 +9257,7 @@ tsdx@0.12.3: tslib "^1.9.3" typescript "^3.7.3" -tslib@1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==