diff --git a/demo/src/App.js b/demo/src/App.js index d8600482..3b27fe79 100644 --- a/demo/src/App.js +++ b/demo/src/App.js @@ -26,6 +26,7 @@ import strict from './strict'; import reactRedux from './reactRedux'; import styledComponents from './styledComponents'; import logOwnerReasons from './logOwnerReasons'; +import mobx from './mobx'; const demosList = { bigList, @@ -48,6 +49,7 @@ const demosList = { reactReduxHOC, styledComponents, logOwnerReasons, + mobx, }; const defaultDemoName = 'bigList'; diff --git a/demo/src/mobx/index.js b/demo/src/mobx/index.js new file mode 100644 index 00000000..f7b1d160 --- /dev/null +++ b/demo/src/mobx/index.js @@ -0,0 +1,58 @@ +import React from 'react'; +import ReactDom from 'react-dom'; +import _ from 'lodash'; +import { makeAutoObservable } from 'mobx'; +import { observer } from 'mobx-react-lite'; + +export default { + description: 'Mobx', + fn({ domElement, whyDidYouRender }) { + whyDidYouRender(React); + + class TestStore { + state = { nested: { value: '0' } }; + + constructor() { + makeAutoObservable(this); + } + + increaseCount() { + this.state.nested.value++; + } + + deepEqualsCount() { + this.state = _.cloneDeep(this.state); + } + } + + const SimpleComponent = ({ testStore }) => { + // eslint-disable-next-line no-console + console.log('re-render!'); + + return ( +
+ count: {testStore.state.nested.value} + + +
+ ); + }; + + const ObservedSimpleComponent = observer(SimpleComponent); + + SimpleComponent.whyDidYouRender = true; + ObservedSimpleComponent.whyDidYouRender = true; + + const testStore = new TestStore(); + + const Main = () => ( + + ); + + ReactDom.render(
, domElement); + }, +}; diff --git a/package.json b/package.json index a9528694..168fabac 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,8 @@ "jest": "^26.6.3", "jest-cli": "^26.6.3", "magic-string": "^0.25.7", + "mobx": "^6.0.4", + "mobx-react-lite": "^3.1.6", "nollup": "^0.14.4", "react": "^17.0.1", "react-16": "npm:react@^16.14.0", @@ -111,6 +113,7 @@ "rollup-plugin-commonjs-alternate": "^0.7.2", "rollup-plugin-license": "^2.2.0", "rollup-plugin-node-resolve": "^5.2.0", + "snapshot-diff": "^0.8.1", "start-server-and-test": "^1.11.6", "styled-components": "^5.2.1", "typescript": "^4.1.3" diff --git a/tests/getUpdateInfo.test.js b/tests/getUpdateInfo.test.js index 748a6a09..60847e4f 100644 --- a/tests/getUpdateInfo.test.js +++ b/tests/getUpdateInfo.test.js @@ -321,7 +321,7 @@ describe('getUpdateInfo', () => { }); test('Props change by function', () => { - const input = { + const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: { a: () => {} }, diff --git a/tests/librariesTests/mobx.test.js b/tests/librariesTests/mobx.test.js index 26baa8e0..c1cc30f3 100644 --- a/tests/librariesTests/mobx.test.js +++ b/tests/librariesTests/mobx.test.js @@ -1,33 +1,48 @@ import React from 'react'; -import { createStore } from 'redux'; -import * as Redux from 'react-redux'; -import { connect, Provider } from 'react-redux'; -import { cloneDeep } from 'lodash'; + +import { makeAutoObservable } from 'mobx'; +import { observer } from 'mobx-react-lite'; + import * as rtl from '@testing-library/react'; -import { diffTypes } from 'consts'; import whyDidYouRender from 'index'; +import { diffTypes } from 'consts'; + +describe('mobx-react-lite', () => { + const getInitialState = () => ({ nested: { value: '0' } }); + const getDifferentState1 = () => ({ nested: { value: '1' } }); + const getDifferentState2 = () => ({ nested: { value: '2' } }); + + class TestStore { + state = getInitialState(); + + constructor() { + makeAutoObservable(this); + } -describe('react-redux - simple', () => { - const initialState = { a: { b: 'c' } }; + setSameState() { + // eslint-disable-next-line no-self-assign + this.state = this.state; + } - const rootReducer = (state, action) => { - if (action.type === 'differentState') { - return { a: { b: 'd' } }; + setDifferentState1() { + this.state = getDifferentState1(); } - if (action.type === 'deepEqlState') { - return cloneDeep(state); + setDifferentState2() { + this.state = getDifferentState2(); } - return state; - }; + setDeepEqualsState() { + this.state = getInitialState(); + } + } - let store; + let testStore; let updateInfos; beforeEach(() => { - store = createStore(rootReducer, initialState); + testStore = new TestStore; updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), @@ -40,269 +55,135 @@ describe('react-redux - simple', () => { } }); - test('same state after dispatch', () => { - const SimpleComponent = ({ a }) => ( -
{a.b}
+ test('change to different state', () => { + const SimpleComponent = ({ testStore }) => ( +
+ hi! + {testStore.state.nested.value} + +
); - const ConnectedSimpleComponent = connect( - state => ({ a: state.a }) - )(SimpleComponent); - SimpleComponent.whyDidYouRender = true; + const ObservedSimpleComponent = observer(SimpleComponent); + + ObservedSimpleComponent.whyDidYouRender = true; const Main = () => ( - - - + ); - rtl.render(
); + const { getByTestId } = rtl.render(
); - expect(store.getState().a.b).toBe('c'); + expect(testStore.state.nested.value).toBe('0'); - rtl.act(() => { - store.dispatch({ type: 'sameState' }); - }); - - expect(store.getState().a.b).toBe('c'); - - expect(updateInfos).toHaveLength(0); - }); - - test('different state after dispatch', () => { - const SimpleComponent = ({ a }) => ( -
{a.b}
- ); - const ConnectedSimpleComponent = connect( - state => ({ a: state.a }) - )(SimpleComponent); - - SimpleComponent.whyDidYouRender = true; - - const Main = () => ( - - - - ); + rtl.fireEvent.click(getByTestId('set-different-state-1-button')); - rtl.render(
); + expect(testStore.state.nested.value).toBe('1'); - expect(store.getState().a.b).toBe('c'); + testStore.setDifferentState2(); - rtl.act(() => { - store.dispatch({ type: 'differentState' }); - }); + expect(testStore.state.nested.value).toBe('2'); - expect(store.getState().a.b).toBe('d'); + expect(updateInfos).toHaveLength(2); - expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ - propsDifferences: [ - expect.objectContaining({ diffType: diffTypes.different }), + propsDifferences: false, + stateDifferences: false, + hookDifferences: [ expect.objectContaining({ diffType: diffTypes.different }), ], + ownerDifferences: false, + }); + + expect(updateInfos[1].reason).toEqual({ + propsDifferences: false, stateDifferences: false, - hookDifferences: false, - ownerDifferences: { - hookDifferences: false, - propsDifferences: false, - stateDifferences: false, - }, + hookDifferences: [ + expect.objectContaining({ diffType: diffTypes.different }), + ], + ownerDifferences: false, }); }); - test('deep equals state after dispatch', () => { - const SimpleComponent = ({ a }) => ( -
- {a.b} + test('change to same state', () => { + const SimpleComponent = ({ testStore }) => ( +
+ hi! + {testStore.state.nested.value} +
); - const ConnectedSimpleComponent = connect( - state => ({ a: state.a }) - )(SimpleComponent); - SimpleComponent.whyDidYouRender = true; + const ObservedSimpleComponent = observer(SimpleComponent); + + ObservedSimpleComponent.whyDidYouRender = true; const Main = () => ( - - - + ); - rtl.render(
); - - expect(store.getState().a.b).toBe('c'); - - rtl.act(() => { - store.dispatch({ type: 'deepEqlState' }); - }); - - expect(store.getState().a.b).toBe('c'); - - expect(updateInfos).toHaveLength(1); - expect(updateInfos[0].reason).toEqual({ - propsDifferences: [ - expect.objectContaining({ diffType: diffTypes.deepEquals }), - ], - stateDifferences: false, - hookDifferences: false, - ownerDifferences: { - hookDifferences: false, - propsDifferences: false, - stateDifferences: false, - }, - }); - }); -}); - -describe('react-redux - hooks', () => { - const initialState = { a: { b: 'c' } }; + const { getByTestId, rerender } = rtl.render(
); - const rootReducer = (state, action) => { - if (action.type === 'differentState') { - return { a: { b: 'd' } }; - } - - if (action.type === 'deepEqlState') { - return cloneDeep(state); - } + expect(testStore.state.nested.value).toBe('0'); - return state; - }; + rtl.fireEvent.click(getByTestId('set-same-state')); - let store; - let updateInfos; + testStore.setSameState(); - beforeEach(() => { - store = createStore(rootReducer, initialState); - updateInfos = []; - whyDidYouRender(React, { - notifier: updateInfo => updateInfos.push(updateInfo), - trackExtraHooks: [ - [Redux, 'useSelector'], - ], - }); - }); + rerender(); - afterEach(() => { - if (React.__REVERT_WHY_DID_YOU_RENDER__) { - React.__REVERT_WHY_DID_YOU_RENDER__(); - } + expect(updateInfos).toHaveLength(0); }); - test('same state after dispatch', () => { - const ConnectedSimpleComponent = () => { - const a = Redux.useSelector(state => state); - return ( -
{a.b}
- ); - }; - ConnectedSimpleComponent.whyDidYouRender = true; - - const Main = () => ( - - - + test('change to deepEquals state', () => { + const SimpleComponent = ({ testStore }) => ( +
+ hi! + {testStore.state.nested.value} + +
); - rtl.render(
); - - expect(store.getState().a.b).toBe('c'); - - rtl.act(() => { - store.dispatch({ type: 'sameState' }); - }); - - expect(store.getState().a.b).toBe('c'); + const ObservedSimpleComponent = observer(SimpleComponent); - expect(updateInfos).toHaveLength(0); - }); - - test('different state after dispatch', () => { - const ConnectedSimpleComponent = () => { - const a = Redux.useSelector(state => state.a); - return ( -
{a.b}
- ); - }; - - ConnectedSimpleComponent.whyDidYouRender = true; + SimpleComponent.whyDidYouRender = true; const Main = () => ( - - - + ); - rtl.render(
); + const { getByTestId } = rtl.render(
); - expect(store.getState().a.b).toBe('c'); + expect(testStore.state.nested.value).toBe('0'); - rtl.act(() => { - store.dispatch({ type: 'differentState' }); - }); + rtl.fireEvent.click(getByTestId('set-deep-equals-state-button')); - expect(store.getState().a.b).toBe('d'); + testStore.setDeepEqualsState(); expect(updateInfos).toHaveLength(2); - expect(updateInfos[0]).toEqual(expect.objectContaining({ - hookName: 'useReducer', // react-redux inner hook - })); - expect(updateInfos[1]).toEqual(expect.objectContaining({ - hookName: 'useSelector', - reason: { - propsDifferences: false, - stateDifferences: false, - hookDifferences: [ - { diffType: diffTypes.different, pathString: '.b', prevValue: 'c', nextValue: 'd' }, - { diffType: diffTypes.different, pathString: '', prevValue: { b: 'c' }, nextValue: { b: 'd' } }, - ], - ownerDifferences: false, - }, - })); - }); - - test('deep equals state after dispatch', () => { - const ConnectedSimpleComponent = () => { - const a = Redux.useSelector(state => state.a); - return ( -
- {a.b} -
- ); - }; - ConnectedSimpleComponent.whyDidYouRender = true; - - const Main = () => ( - - - - ); - - rtl.render(
); - expect(store.getState().a.b).toBe('c'); - - rtl.act(() => { - store.dispatch({ type: 'deepEqlState' }); + expect(updateInfos[0].reason).toEqual({ + propsDifferences: false, + stateDifferences: false, + hookDifferences: [ + expect.objectContaining({ diffType: diffTypes.different }), + ], + ownerDifferences: false, }); - expect(store.getState().a.b).toBe('c'); - - expect(updateInfos).toHaveLength(2); - expect(updateInfos[0]).toEqual(expect.objectContaining({ - hookName: 'useReducer', // react-redux inner hook - })); - expect(updateInfos[1]).toEqual(expect.objectContaining({ - hookName: 'useSelector', - reason: { - propsDifferences: false, - stateDifferences: false, - hookDifferences: [ - { diffType: diffTypes.deepEquals, pathString: '', prevValue: { b: 'c' }, nextValue: { b: 'c' } }, - ], - ownerDifferences: false, - }, - })); + expect(updateInfos[1].reason).toEqual({ + propsDifferences: false, + stateDifferences: false, + hookDifferences: [ + expect.objectContaining({ diffType: diffTypes.deepEquals }), + ], + ownerDifferences: false, + }); }); }); diff --git a/yarn.lock b/yarn.lock index 2c715441..89b518dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4334,7 +4334,7 @@ jest-config@^26.6.3: micromatch "^4.0.2" pretty-format "^26.6.2" -jest-diff@^26.0.0, jest-diff@^26.6.2: +jest-diff@^26.0.0, jest-diff@^26.1.0, jest-diff@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== @@ -4578,7 +4578,7 @@ jest-serializer@^26.6.2: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^26.6.2: +jest-snapshot@^26.1.0, jest-snapshot@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== @@ -5174,6 +5174,16 @@ mkdirp@^0.5.4: dependencies: minimist "^1.2.5" +mobx-react-lite@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/mobx-react-lite/-/mobx-react-lite-3.1.6.tgz#e7f4809ab66edd1acca5adb00c6b88c600ae1952" + integrity sha512-MM3x9BLt5nC7iE/ILA5n2+hfrplEKYbFjqROEuGkzBdZP/KD+Z44+2gseczRrTG0xFuiPWfEzgT68+6/zqOiEw== + +mobx@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-6.0.4.tgz#8fc3e3629a3346f8afddf5bd954411974744dad1" + integrity sha512-wT2QJT9tW19VSHo9x7RPKU3z/I2Ps6wUS8Kb1OO+kzmg7UY3n4AkcaYG6jq95Lp1R9ohjC/NGYuT2PtuvBjhFg== + moment@2.27.0: version "2.27.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" @@ -5703,7 +5713,7 @@ pretty-bytes@^5.4.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== -pretty-format@^26.0.0, pretty-format@^26.6.2: +pretty-format@^26.0.0, pretty-format@^26.1.0, pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== @@ -6537,6 +6547,15 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +snapshot-diff@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/snapshot-diff/-/snapshot-diff-0.8.1.tgz#09cc28849a6cee4073818d81dfd9f005e72632ca" + integrity sha512-vPb0JHikbZM8kK4A/ktIEbM5FwHEgGQ330ARxgaccDcVgapY73sjKpK03uxJGHb0eDViFWTch0OY3ax52AIKGw== + dependencies: + jest-diff "^26.1.0" + jest-snapshot "^26.1.0" + pretty-format "^26.1.0" + "source-map-fast@npm:source-map@0.7.3", source-map@^0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"