Repository: welldone-software/why-did-you-render Branch: master Commit: 3ec3512d750c Files: 129 Total size: 259.1 KB Directory structure: gitextract_f4isy9w2/ ├── .github/ │ ├── actions/ │ │ └── setup/ │ │ └── action.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── .husky/ │ ├── .gitignore │ └── pre-commit ├── .npmrc ├── .run/ │ └── Main Jest.run.xml ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── LICENSE ├── README.md ├── babel.config.cjs ├── cypress/ │ ├── .eslintrc │ ├── babel.config.js │ ├── e2e/ │ │ ├── big_list.js │ │ ├── child-of-pure-component.js │ │ ├── clone-element.js │ │ ├── create-factory.js │ │ ├── hooks-use-context.js │ │ ├── hooks-use-memo-and-callback-child.js │ │ ├── hooks-use-reducer.js │ │ ├── hooks-use-state.js │ │ ├── hot-reload.js │ │ ├── no-change.js │ │ ├── owner-reasons.js │ │ ├── props-and-state-change.js │ │ ├── props-changes.js │ │ ├── react-redux.js │ │ ├── special-changes.js │ │ ├── ssr.js │ │ ├── state-changes.js │ │ ├── strict-mode.js │ │ ├── styled-component.js │ │ └── test_console_assertions.js │ ├── fixtures/ │ │ └── example.json │ ├── plugins/ │ │ └── index.js │ └── support/ │ ├── assertions.js │ ├── commands.js │ └── e2e.js ├── cypress.config.ts ├── demo/ │ ├── nollup.config.js │ ├── public/ │ │ └── index.html │ ├── serve.js │ └── src/ │ ├── App.js │ ├── Menu.js │ ├── bigList/ │ │ └── index.js │ ├── bothChanges/ │ │ └── index.js │ ├── childOfPureComponent/ │ │ └── index.js │ ├── cloneElement/ │ │ └── index.js │ ├── createFactory/ │ │ └── index.js │ ├── createStepLogger.js │ ├── forwardRef/ │ │ └── index.js │ ├── hooks/ │ │ ├── useContext.js │ │ ├── useMemoAndCallbackChild.js │ │ ├── useReducer.js │ │ └── useState.js │ ├── hotReload/ │ │ └── index.js │ ├── index.js │ ├── logOwnerReasons/ │ │ └── index.js │ ├── noChanges/ │ │ └── index.js │ ├── propsChanges/ │ │ └── index.js │ ├── reactRedux/ │ │ └── index.js │ ├── reactReduxHOC/ │ │ └── index.js │ ├── specialChanges/ │ │ └── index.js │ ├── ssr/ │ │ ├── DemoComponent.js │ │ └── index.js │ ├── stateChanges/ │ │ └── index.js │ ├── strict/ │ │ └── index.js │ └── styledComponents/ │ └── index.js ├── eslint.config.js ├── jest.config.js ├── jest.polyfills.js ├── jestSetup.js ├── jsx-dev-runtime.d.ts ├── jsx-dev-runtime.js ├── jsx-runtime.d.ts ├── jsx-runtime.js ├── package.json ├── rollup.config.js ├── src/ │ ├── calculateDeepEqualDiffs.js │ ├── consts.js │ ├── defaultNotifier.js │ ├── findObjectsDifferences.js │ ├── getDefaultProps.js │ ├── getDisplayName.js │ ├── getUpdateInfo.js │ ├── helpers.js │ ├── index.js │ ├── normalizeOptions.js │ ├── patches/ │ │ ├── patchClassComponent.js │ │ ├── patchForwardRefComponent.js │ │ ├── patchFunctionalOrStrComponent.js │ │ └── patchMemoComponent.js │ ├── printDiff.js │ ├── shouldTrack.js │ ├── utils.js │ ├── wdyrStore.js │ └── whyDidYouRender.js ├── tests/ │ ├── .eslintrc │ ├── babel.config.cjs │ ├── calculateDeepEqualDiffs.test.js │ ├── defaultNotifier.test.js │ ├── findObjectsDifferences.test.js │ ├── getDisplayName.test.js │ ├── getUpdateInfo.test.js │ ├── hooks/ │ │ ├── childrenUsingHookResults.test.js │ │ ├── hooks.test.js │ │ ├── useContext.test.js │ │ ├── useReducer.test.js │ │ ├── useState.test.js │ │ └── useSyncExternalStore.test.js │ ├── index.test.js │ ├── librariesTests/ │ │ ├── react-redux.test.js │ │ ├── react-router-dom.test.js │ │ └── styled-components.test.js │ ├── logOnDifferentValues.test.js │ ├── logOwnerReasons.test.js │ ├── normalizeOptions.test.js │ ├── patches/ │ │ ├── patchClassComponent.test.js │ │ ├── patchForwardRefComponent.test.js │ │ ├── patchFunctionalOrStrComponent.test.js │ │ └── patchMemoComponent.test.js │ ├── shouldTrack.test.js │ ├── strictMode.test.js │ └── utils.test.js ├── tsconfig.json ├── tsx-test.tsx └── types.d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/actions/setup/action.yml ================================================ name: 'Setup Node.js and dependencies' description: 'Sets up Node.js, caches dependencies, and installs packages' runs: using: "composite" steps: - uses: actions/checkout@v4 - name: Use Node.js LTS uses: actions/setup-node@v4 with: node-version: 'lts/*' - name: Cache dependencies uses: actions/cache@v4 with: path: ~/.cache key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - run: yarn install shell: bash ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: push: branches: master pull_request: branches: master jobs: cypress-tests: runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - name: Run Cypress tests run: yarn cypress:test unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - run: yarn test:ci lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - run: yarn lint audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - run: yarn audit ================================================ FILE: .gitignore ================================================ .idea .temp .cache *.swo *.swp *.log node_modules dist cypress/videos coverage .history ================================================ FILE: .husky/.gitignore ================================================ _ ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" yarn test ================================================ FILE: .npmrc ================================================ message="version v%s" ================================================ FILE: .run/Main Jest.run.xml ================================================ ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "coenraads.bracket-pair-colorizer", "dbaeumer.vscode-eslint", "eamodio.gitlens", "christian-kohler.npm-intellisense", "vscode-icons-team.vscode-icons", "jpoissonnier.vscode-styled-components", "ofhumanbondage.react-proptypes-intellisense", "christian-kohler.path-intellisense", "saharavr.react-component-splitter", "mhutchie.git-graph", "dsznajder.es7-react-js-snippets", "github.vscode-github-actions" ] } ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "name": "vscode-jest-tests", "request": "launch", "program": "${workspaceFolder}/node_modules/jest/bin/jest", "args": [ "--runInBand", ], "cwd": "${workspaceFolder}", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "disableOptimisticBPs": true }, { "type": "node", "name": "vscode-jest-tests-no-cache", "request": "launch", "program": "${workspaceFolder}/node_modules/jest/bin/jest", "args": [ "--runInBand", "--watch", "--no-cache" ], "cwd": "${workspaceFolder}", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "disableOptimisticBPs": true } ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.snippetSuggestions": "top", "editor.trimAutoWhitespace": true, "editor.tabSize": 2, "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], "eslint.alwaysShowStatus": true, "eslint.format.enable": true, "eslint.codeActionsOnSave.mode": "problems", "editor.formatOnSave": true, "flow.enabled": false, "typescript.tsdk": "./node_modules/typescript/lib", "typescript.locale": "en", "typescript.preferences.quoteStyle": "single", "jestrunner.debugOptions": {"args": ["--watch"]}, "jestrunner.configPath": "jest.config.js", "cSpell.words": [ "astring", "lcov", "nollup", "postversion", "Vitali", "WDYR", "welldone", "Zaidman" ], } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018-present, Vitali Zaidman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

# Why Did You Render [![npm version](https://badge.fury.io/js/%40welldone-software%2Fwhy-did-you-render.svg)](https://badge.fury.io/js/%40welldone-software%2Fwhy-did-you-render) [![Build Status](https://github.com/welldone-software/why-did-you-render/actions/workflows/main.yml/badge.svg)](https://github.com/welldone-software/why-did-you-render/actions/workflows/main.yml) [![license](https://img.shields.io/npm/l/@welldone-software/why-did-you-render?style=flat)](https://github.com/welldone-software/why-did-you-render/blob/master/LICENSE) [![@welldone-software/why-did-you-render](https://snyk.io/advisor/npm-package/@welldone-software/why-did-you-render/badge.svg)](https://snyk.io/advisor/npm-package/@welldone-software/why-did-you-render) [![Coverage Status](https://coveralls.io/repos/github/welldone-software/why-did-you-render/badge.svg?branch=add-e2e-tests-using-cypress)](https://coveralls.io/github/welldone-software/why-did-you-render?branch=add-e2e-tests-using-cypress) `why-did-you-render` by [Welldone Software](https://welldone.software/) monkey patches **`React`** to notify you about potentially avoidable re-renders. (Works with **`React Native`** as well.) For example, if you pass `style={{width: '100%'}}` to a big memo component it would always re-render on every element creation: ```jsx ``` It can also help you to simply track when and why a certain component re-renders. > [!CAUTION] > The library was not tested with [React Compiler](https://react.dev/learn/react-compiler) at all. I believe it's completely incompatible with it. > [!CAUTION] > Not all re-renders are *"bad"*. Sometimes shenanigan to reduce re-renders can either hurt your App's performance or have a negligible effect, in which case it would be just a waste of your efforts, and complicate your code. Try to focus on heavier components when optimizing and use the [React DevTools Profiler](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html) to measure the effects of any changes. > [!NOTE] I've joined the React team, specifically working on React tooling. This role has opened up exciting opportunities to enhance the developer experience for React users— and your input could offer valuable insights to help me with this effort. Please join the conversation in the [discussion thread](https://github.com/welldone-software/why-did-you-render/discussions/309)! ## Setup The latest version of the library was tested [(unit tests and E2E)]((https://travis-ci.com/welldone-software/why-did-you-render.svg?branch=master)) with **`React@19`** only. * [For `React 18`, please see the readme for version @^8](https://github.com/welldone-software/why-did-you-render/tree/version-8). * [For `React 17` and `React 16`, please see the readme for version @^7](https://github.com/welldone-software/why-did-you-render/tree/version-7). ``` npm install @welldone-software/why-did-you-render --save-dev ``` or ``` yarn add @welldone-software/why-did-you-render -D ``` Set the library to be the React's importSource and make sure `preset-react` is in `development` mode. This is because `React 19` requires using the `automatic` [JSX transformation](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html). ```js ['@babel/preset-react', { runtime: 'automatic', development: process.env.NODE_ENV === 'development', importSource: '@welldone-software/why-did-you-render', }] ``` ### React Native #### Bare workflow Add the plugin as listed below and start react-native packager as usual. Default env for babel is "development". If you do not use expo when working with react-native, the following method will help you. ```js module.exports = { presets: ['module:metro-react-native-babel-preset'], env: { development: { plugins: [['@babel/plugin-transform-react-jsx', { runtime: 'automatic', development: process.env.NODE_ENV === 'development', importSource: '@welldone-software/why-did-you-render', }]], }, }, } ``` #### Expo managed You can pass params to `@babel/preset-react` through `babel-preset-expo` ```js // babel.config.js module.exports = function (api) { api.cache(true); return { presets: [ [ "babel-preset-expo", { jsxImportSource: "@welldone-software/why-did-you-render", }, ], ], }; }; ``` > Notice: Create React App (CRA) ^4 **uses the `automatic` JSX transformation.** > [See the following comment on how to do this step with CRA](https://github.com/welldone-software/why-did-you-render/issues/154#issuecomment-773905769) Create a `wdyr.js` file and import it as **the very first import** in your application. `wdyr.js`: ```jsx import React from 'react'; if (process.env.NODE_ENV === 'development') { const whyDidYouRender = require('@welldone-software/why-did-you-render'); whyDidYouRender(React, { trackAllPureComponents: true, }); } ``` > [!CAUTION] > The library should *NEVER* be used in production because: > - It significantly slows down React > - It monkey patches React and can result in unexpected behavior In [Typescript](https://github.com/welldone-software/why-did-you-render/issues/161), call the file wdyr.ts and add the following line to the top of the file to import the package's types: ```tsx /// ``` Import `wdyr` as the first import (even before `react-hot-loader` if you use it): `index.js`: ```jsx import './wdyr'; // <--- first import import 'react-hot-loader'; import React from 'react'; import ReactDOM from 'react-dom'; // ... import {App} from './app'; // ... ReactDOM.render(, document.getElementById('root')); ``` If you use `trackAllPureComponents`, all pure components ([React.PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent) or [React.memo](https://reactjs.org/docs/react-api.html#reactmemo)) will be tracked. Otherwise, add `whyDidYouRender = true` to ad-hoc components to track them. (f.e `Component.whyDidYouRender = true`) More information about what is tracked can be found in [Tracking Components](#tracking-components). Can't see any WDYR logs? Check out the [troubleshooting section](#troubleshooting) or search in the [issues](https://github.com/welldone-software/why-did-you-render/issues). ## Custom Hooks Also, tracking custom hooks is possible by using `trackExtraHooks`. For example if you want to track `useSelector` from React Redux: `wdyr.js`: ```jsx import React from 'react'; // For react-native you might want to use // the __DEV__ flag instead of process.env.NODE_ENV === 'development' if (process.env.NODE_ENV === 'development') { const whyDidYouRender = require('@welldone-software/why-did-you-render'); const ReactRedux = require('react-redux'); whyDidYouRender(React, { trackAllPureComponents: true, trackExtraHooks: [ [ReactRedux, 'useSelector'] ] }); } ``` > Notice that there's currently a problem with rewriting exports of imported files in `webpack`. A quick workaround can help with it: [#85 - trackExtraHooks cannot set property](https://github.com/welldone-software/why-did-you-render/issues/85). ## Read More * [Why Did You Render Mr. Big Pure React Component???](http://bit.ly/wdyr1) * [**Common fixing scenarios** this library can help with](http://bit.ly/wdyr02) * [**React Hooks** - Understand and fix hooks issues](http://bit.ly/wdyr3) * [Why Did You Render v4 Released!](https://medium.com/welldone-software/why-did-you-render-v4-released-48e0f0b99d4c) - TypeScript support, Custom hooks tracking (like React-Redux’s useSelector), Tracking of all pure components. ## Integration With Other Libraries * [Next.js example](https://github.com/zeit/next.js/tree/canary/examples/with-why-did-you-render) * [React-Redux With Hooks](https://medium.com/welldone-software/why-did-you-render-v4-released-48e0f0b99d4c) * [Mobx is currently not supported](https://github.com/welldone-software/why-did-you-render/issues/162) * [React-Native flipper plugin made by @allen-hsu](https://github.com/allen-hsu/wdyr-flipper#wdry-flipper-reporter) ## Sandbox You can test the library in [the official sandbox](http://bit.ly/wdyr-sb). And another [official sandbox with hooks tracking](https://codesandbox.io/s/why-did-you-render-sandbox-with-hooks-pyi14) ## Tracking Components You can track all pure components ([React.PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent) or [React.memo](https://reactjs.org/docs/react-api.html#reactmemo)) using the `trackAllPureComponents: true` option. You can also manually track any component you want by setting `whyDidYouRender` on them like this: ```js class BigList extends React.Component { static whyDidYouRender = true render(){ return ( //some heavy render you want to ensure doesn't happen if its not necessary ) } } ``` Or for functional components: ```js const BigListPureComponent = props => (
//some heavy component you want to ensure doesn't happen if its not necessary
) BigListPureComponent.whyDidYouRender = true ``` You can also pass an object to specify more advanced tracking settings: ```js EnhancedMenu.whyDidYouRender = { logOnDifferentValues: true, customName: 'Menu' } ``` - `logOnDifferentValues`: Normally, only re-renders that are caused by equal values in props / state trigger notifications: ```js render() render() ``` This option will trigger notifications even if they occurred because of different props / state (Thus, because of "legit" re-renders): ```js render() render() ``` - `customName`: Sometimes the name of the component can be missing or very inconvenient. For example: ```js withPropsOnChange(withPropsOnChange(withStateHandlers(withPropsOnChange(withState(withPropsOnChange(lifecycle(withPropsOnChange(withPropsOnChange(onlyUpdateForKeys(LoadNamespace(Connect(withState(withState(withPropsOnChange(lifecycle(withPropsOnChange(withHandlers(withHandlers(withHandlers(withHandlers(Connect(lifecycle(Menu))))))))))))))))))))))) ``` ## Options Optionally you can pass in `options` as the second parameter. The following options are available: - `include: [RegExp, ...]` (`null` by default) - `exclude: [RegExp, ...]` (`null` by default) - `trackAllPureComponents: false` - `trackHooks: true` - `trackExtraHooks: []` - `logOwnerReasons: true` - `logOnDifferentValues: false` - `hotReloadBufferMs: 500` - `onlyLogs: false` - `collapseGroups: false` - `titleColor` - `diffNameColor` - `diffPathColor` - `textBackgroundColor` - `notifier: ({Component, displayName, hookName, prevProps, prevState, prevHookResult, nextProps, nextState, nextHookResult, reason, options, ownerDataMap}) => void` - `getAdditionalOwnerData: (element) => {...}` #### include / exclude ##### (default: `null`) You can include or exclude tracking of components by their displayName using the `include` and `exclude` options. For example, the following code is used to [track all redundant re-renders that are caused by older React-Redux](http://bit.ly/wdyr04): ```js whyDidYouRender(React, { include: [/^ConnectFunction/] }); ``` > *Notice: **exclude** takes priority over both `include` and manually set `whyDidYouRender = `* #### trackAllPureComponents ##### (default: `false`) You can track all pure components (both `React.memo` and `React.PureComponent` components) > *Notice: You can exclude the tracking of any specific component with `whyDidYouRender = false`* #### trackHooks ##### (default: `true`) You can turn off tracking of hooks changes. [Understand and fix hook issues](http://bit.ly/wdyr3). #### trackExtraHooks ##### (default: `[]`) Track custom hooks: ```js whyDidYouRender(React, { trackExtraHooks: [ // notice that 'useSelector' is a named export [ReactRedux, 'useSelector'], ] }); ``` > This feature is rewriting exports of imported files. There is currently a problem with that approach in webpack. A workaround is available here: [#85 - trackExtraHooks cannot set property](https://github.com/welldone-software/why-did-you-render/issues/85) #### logOwnerReasons ##### (default: `true`) One way of fixing re-render issues is preventing the component's owner from re-rendering. This option is `true` by default and it lets you view the reasons why an owner component re-renders. ![demo](images/logOwnerReasons.png) #### logOnDifferentValues ##### (default: `false`) Normally, you only want logs about component re-renders when they could have been avoided. With this option, it is possible to track all re-renders. For example: ```js render() render() // will only log if you use {logOnDifferentValues: true} ``` #### hotReloadBufferMs ##### (default: `500`) Time in milliseconds to ignore updates after a hot reload is detected. When a hot reload is detected, we ignore all updates for `hotReloadBufferMs` to not spam the console. #### onlyLogs ##### (default: `false`) If you don't want to use `console.group` to group logs you can print them as simple logs. #### collapseGroups ##### (default: `false`) Grouped logs can be collapsed. #### titleColor / diffNameColor / diffPathColor / textBackgroundColor ##### (default titleColor: `'#058'`) ##### (default diffNameColor: `'blue'`) ##### (default diffPathColor: `'red'`) ##### (default textBackgroundColor: `'white`) Controls the colors used in the console notifications #### notifier ##### (default: defaultNotifier that is exposed from the library) You can create a custom notifier if the default one does not suite your needs. #### getAdditionalOwnerData ##### (default: `undefined`) You can provide a function that harvests additional data from the original react element. The object returned from this function will be added to the ownerDataMap which can be accessed later within your notifier function override. ## Troubleshooting ### No tracking * If you are in production, WDYR is probably disabled. * Maybe no component is tracked * Check out [Tracking Components](#tracking-components) once again. * If you only track pure components using `trackAllPureComponents: true` then you would only track either ([React.PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent) or [React.memo](https://reactjs.org/docs/react-api.html#reactmemo)), maybe none of your components are pure so none of them will get tracked. * Maybe you have no issues * Try causing an issue by temporary rendering the whole app twice in it's entry point: `index.js`: ```jsx const HotApp = hot(App); HotApp.whyDidYouRender = true; ReactDOM.render(, document.getElementById('root')); ReactDOM.render(, document.getElementById('root')); ``` ### Custom Hooks tracking (like useSelector) There's currently a problem with rewriting exports of imported files in `webpack`. A quick workaround can help with it: [#85 - trackExtraHooks cannot set property](https://github.com/welldone-software/why-did-you-render/issues/85). ### React-Redux `connect` HOC is spamming the console Since `connect` hoists statics, if you add WDYR to the inner component, it is also added to the HOC component where complex hooks are running. To fix this, add the `whyDidYouRender = true` static to a component after the connect: ```js const SimpleComponent = ({a}) =>
{a.b}
) // not before the connect: // SimpleComponent.whyDidYouRender = true const ConnectedSimpleComponent = connect( state => ({a: state.a}) )(SimpleComponent) // after the connect: SimpleComponent.whyDidYouRender = true ``` ### Sourcemaps To see the library's sourcemaps use the [source-map-loader](https://webpack.js.org/loaders/source-map-loader/). ## Credit Inspired by the following previous work: * github.com/maicki/why-did-you-update (no longer public) which I had the chance to maintain for some time. * https://github.com/garbles/why-did-you-update where [A deep dive into React perf debugging](https://benchling.engineering/a-deep-dive-into-react-perf-debugging-fd2063f5a667/) is credited for the idea. ## License This library is [MIT licensed](./LICENSE). [🔼Back to top!](#Why-Did-You-Render) ================================================ FILE: babel.config.cjs ================================================ const compact = require('lodash/compact'); module.exports = function(api) { const isProd = process.env.NODE_ENV === 'production'; const isTest = process.env.NODE_ENV === 'test'; api.cache(false); const presets = [ ['@babel/preset-env', { modules: isTest ? 'commonjs' : false, }], ['@babel/preset-react', { runtime: 'automatic', development: true, importSource: `${__dirname}`, }], ]; const plugins = compact([ (!isProd && !isTest) && 'react-refresh/babel', ]); return {presets, plugins}; }; ================================================ FILE: cypress/.eslintrc ================================================ { "extends": [ "plugin:cypress/recommended" ] } ================================================ FILE: cypress/babel.config.js ================================================ module.exports = require('../babel.config.cjs').default; ================================================ FILE: cypress/e2e/big_list.js ================================================ it('Big list basic example', () => { cy.visitAndSpyConsole('/#bigList',console => { cy.contains('button', 'Increase!').click(); expect(console.group).to.be.calledWithMatches([ {match: 'BigList', times: 1}, {match: /props.*style\W/, times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because of props changes'], times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/child-of-pure-component.js ================================================ it('Child of Pure Component', () => { cy.visitAndSpyConsole('/#childOfPureComponent', console => { cy.contains('button', 'clicks:').click(); cy.contains('button', 'clicks:').click(); cy.contains('button', 'clicks:').should('contain', '2'); expect(console.group).to.be.calledWithMatches([ {match: 'PureFather', times: 2}, {match: /props.*children\W/, times: 2}, ]); expect(console.log).to.be.calledWithMatches([ {match: 'syntax always produces a *NEW* immutable React element', times: 2}, ]); }); }); ================================================ FILE: cypress/e2e/clone-element.js ================================================ it('Creating react element using React.cloneElement', () => { cy.visitAndSpyConsole('/#cloneElement', console => { expect(console.group).to.be.calledWithMatches([ {match: 'TestComponent', times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/create-factory.js ================================================ it('Creating react element using React.createFactory', () => { cy.visitAndSpyConsole('/#createFactory', console => { expect(console.group).to.be.calledWithMatches([ {match: 'TestComponent', times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/hooks-use-context.js ================================================ it('Hooks - useContext', () => { cy.visitAndSpyConsole('/#useContext', console => { expect(console.group).to.be.calledWithMatches([ {match: /ComponentWithContextHook$/, times: 2}, {match: 'Rendered by Main', times: 1}, {match: 'ComponentWithContextHookInsideMemoizedParent', times: 1}, {match: '[hook useState result]', times: 1}, {match: '[hook useContext result]', times: 2}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1}, {match: [() => true, 'Re-rendered because of hook changes'], times: 3}, ]); }); }); ================================================ FILE: cypress/e2e/hooks-use-memo-and-callback-child.js ================================================ it('Hooks - useMemo and useCallback Child', () => { cy.visitAndSpyConsole('/#useMemoAndCallbackChild', console => { cy.contains('button', 'count: 0').click(); expect(console.group).to.be.calledWithMatches([ {match: 'Comp', times: 2}, {match: /useMemoFn/, times: 2}, {match: /useCallbackFn/, times: 2}, {match: /props.*\..*count/, times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/hooks-use-reducer.js ================================================ it('Hooks - useReducer', () => { const checkConsole = (console, times) => { expect(console.group).to.be.calledWithMatches([ {match: 'Main', times}, {match: '[hook useReducer result]', times}, ]); expect(console.log).to.be.calledWithMatches([ {match: 'different objects that are equal by value.', times}, ]); }; cy.visitAndSpyConsole('/#useReducer', console => { cy.contains('button', 'broken set count').click(); checkConsole(console, 1); cy.contains('button', 'broken set count').click(); checkConsole(console, 2); cy.contains('button', 'correct set count').click(); checkConsole(console, 2); // should not cause a re-render because of a current useRender user }); }); ================================================ FILE: cypress/e2e/hooks-use-state.js ================================================ it('Hooks - useState', () => { cy.visitAndSpyConsole('/#useState', console => { cy.get('button:contains("Re-render")') .should('have.length', 4) .each($btn => { cy.wrap($btn).click(); }); expect(console.group).to.be.calledWithMatches([ {match: 'BrokenHooksPureComponent', times: 2}, {match: '[hook useState result]', times: 2}, ]); }); }); ================================================ FILE: cypress/e2e/hot-reload.js ================================================ it('React Hot Reload Of Tracked Component', () => { cy.visitAndSpyConsole('/#hotReload', console => { expect(console.group).to.be.calledWithMatches([ {match: 'HotExportedDemoComponent', times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/no-change.js ================================================ it('No Changes', () => { cy.visitAndSpyConsole('/#noChanges', console => { expect(console.group).to.be.calledWithMatches([ {match: 'ClassDemo', times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered although props and state objects are the same.'], times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/owner-reasons.js ================================================ it('Log Owner Reasons', () => { cy.visitAndSpyConsole('/#logOwnerReasons', console => { expect(console.group).to.be.calledWithMatches([ {match: 'Child', times: 3}, {match: 'Rendered by Owner', times: 1}, {match: 'Rendered by ClassOwner', times: 1}, {match: 'Rendered by HooksOwner', times: 1}, {match: /props.*a\W/, times: 1}, {match: '[hook useState result]', times: 2}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal'], times: 3}, {match: [() => true, 'Re-rendered because of props changes'], times: 1}, {match: [() => true, 'Re-rendered because of state changes'], times: 1}, {match: [() => true, 'Re-rendered because of hook changes'], times: 2}, {match: 'different objects.', times: 4}, ]); }); }); ================================================ FILE: cypress/e2e/props-and-state-change.js ================================================ it('Props And State Changes', () => { cy.visitAndSpyConsole('/#bothChanges', console => { expect(console.group).to.be.calledWithMatches([ {match: 'ClassDemo', times: 1}, {match: /props.*a\W/, times: 1}, {match: /state.*c\W/, times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: 'different objects that are equal by value.', times: 2}, ]); }); }); ================================================ FILE: cypress/e2e/props-changes.js ================================================ it('props changes', () => { cy.visitAndSpyConsole('/#propsChanges', console => { expect(console.group).to.be.calledWithMatches([ {match: 'ClassDemo', times: 5}, {match: 'Rendered by Main', times: 5}, {match: /props.*a\W/, times: 4}, {match: /props.*containerProps\W/, times: 4}, ]); }); }); ================================================ FILE: cypress/e2e/react-redux.js ================================================ describe('react-redux', () => { it('React Redux', () => { const checkConsole = (console, times) => { expect(console.group).to.be.calledWithMatches([ {match: 'ConnectedSimpleComponent', times}, {match: '[hook useSelector result]', times}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because of hook changes'], times}, ]); }; cy.visitAndSpyConsole('/#reactRedux', console => { cy.contains('button', 'Same State').click(); checkConsole(console, 0); cy.contains('button', 'Deep Equal State').click(); checkConsole(console, 1); cy.contains('button', 'Deep Equal State').click(); checkConsole(console, 2); cy.contains('button', 'Random Object').click(); checkConsole(console, 2); // should not cause a re-render because the random object is different from the older one }); }); it('React Redux HOC', () => { const checkConsole = (console, times) => { expect(console.group).to.be.calledWithMatches([ {match: 'SimpleComponent', times: times * 2}, {match: /props.*a\W/, times}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because of props changes'], times}, {match: 'different objects that are equal by value', times}, ]); }; cy.visitAndSpyConsole('/#reactReduxHOC', console => { cy.contains('button', 'Same State').click(); checkConsole(console, 0); cy.contains('button', 'Deep Equal State').click(); checkConsole(console, 1); cy.contains('button', 'Deep Equal State').click(); checkConsole(console, 2); cy.contains('button', 'Random Object').click(); checkConsole(console, 2); // should not cause a re-render because the random object is different from the older one }); }); }); ================================================ FILE: cypress/e2e/special-changes.js ================================================ it('Special Changes', () => { cy.visitAndSpyConsole('/#specialChanges', console => { expect(console.group).to.be.calledWithMatches([ {match: 'ClassDemo', times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: 'different regular expressions with the same value.', times: 1}, {match: 'different functions with the same name.', times: 1}, {match: 'different date objects with the same value.', times: 1}, {match: 'different React elements (remember that the syntax always produces a *NEW* immutable React element', times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/ssr.js ================================================ it('Server Side (hydrate)', () => { cy.visitAndSpyConsole('/#ssr', console => { cy.contains('hydrated hi'); expect(console.group).to.be.calledWithMatches([ {match: 'DemoComponent', times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/state-changes.js ================================================ it('state changes', () => { cy.visitAndSpyConsole('/#stateChanges', console => { expect(console.group).to.be.calledWithMatches([ {match: 'ClassDemo', times: 2}, {match: /state.*objectKey\W/, times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because the state object itself changed but its values are all equal'], times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/strict-mode.js ================================================ it('Strict mode', () => { cy.visitAndSpyConsole('/#strict', console => { expect(console.group).to.be.calledWithMatches([ {match: 'ClassDemo', times: 3}, {match: 'Rendered by Main', times: 3}, {match: /props.*a\W/, times: 4}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 2}, {match: 'different objects that are equal by value', times: 4}, ]); }); }); ================================================ FILE: cypress/e2e/styled-component.js ================================================ it('styled-components', () => { cy.visitAndSpyConsole('/#styledComponents', console => { cy.get('div:contains("styled-components")') .last() .should('have.css', 'background-color', 'rgb(255, 150, 174)'); expect(console.group).to.be.calledWithMatches([ {match: 'Styled(SimpleComponent)', times: 1}, {match: /props.*a\W/, times: 1}, ]); expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because of props changes'], times: 1}, {match: 'different objects that are equal by value', times: 1}, ]); }); }); ================================================ FILE: cypress/e2e/test_console_assertions.js ================================================ it('Test console testing throws on wrong console appearance amounts', () => { cy.visitAndSpyConsole('/#bigList', console => { cy.contains('button', 'Increase!').click(); expect( () => expect(console.group).to.be.calledWithMatches([ {match: 'BigList', times: 0}, ]) ).to.throw(); expect( () => expect(console.log).to.be.calledWithMatches([ {match: [() => true, 'Re-rendered because of props changes'], times: 0}, ]) ).to.throw(); }); }); ================================================ FILE: cypress/fixtures/example.json ================================================ { "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" } ================================================ FILE: cypress/plugins/index.js ================================================ /// // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) /** * @type {Cypress.PluginConfig} */ module.exports = (on, config) => { // eslint-disable-line no-unused-vars // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config }; ================================================ FILE: cypress/support/assertions.js ================================================ chai.util.addChainableMethod(chai.Assertion.prototype, 'calledWithMatches', function(matchConfigs) { const calls = this._obj.getCalls(); matchConfigs.forEach(matchConfig => { if (!matchConfig.match) { throw new Error('Every item in calledWithMatches should have a match prop'); } const matchedCalls = calls.filter(call => { return call.calledWithMatch(...Cypress._.castArray(matchConfig.match)); }); if ('times' in matchConfig) { chai.assert( matchConfig.times === matchedCalls.length, `${this._obj} was expected to be called with ${matchConfig.match} for ${matchConfig.times} times but got ${matchedCalls.length} times` ); } else { chai.assert( matchedCalls.length > 0, `${this._obj} was expected to be called with ${matchConfig.match}` ); } }); }); ================================================ FILE: cypress/support/commands.js ================================================ // *********************************************** // This example commands.js shows you how to // create various custom commands and overwrite // existing commands. // // For more comprehensive examples of custom // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** // // // -- This is a parent command -- // Cypress.Commands.add("login", (email, password) => { ... }) // // // -- This is a child command -- // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) // // // -- This is a dual command -- // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // // // -- This will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) Cypress.Commands.add('visitAndSpyConsole', (url, cb) => { const context = {}; cy.visit(url, { onBeforeLoad: win => { cy.spy(win.console, 'log'); cy.spy(win.console, 'group'); }, onLoad: win => context.win = win, }); cy.waitFor(context.win) .then(() => cb(context.win.console)); }); ================================================ FILE: cypress/support/e2e.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands'; import './assertions'; // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: cypress.config.ts ================================================ import { defineConfig } from 'cypress' export default defineConfig({ projectId: 'k4cvdh', e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. setupNodeEvents(on, config) { return require('./cypress/plugins/index.js')(on, config) }, baseUrl: 'http://localhost:3003', specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', }, }) ================================================ FILE: demo/nollup.config.js ================================================ const replace = require('@rollup/plugin-replace'); const babel = require('@rollup/plugin-babel').default; const nodeResolve = require('rollup-plugin-node-resolve'); const alias = require('rollup-plugin-alias'); const commonjs = require('rollup-plugin-commonjs-alternate'); const refresh = require('rollup-plugin-react-refresh'); module.exports = { input: 'demo/src/index.js', output: { file: 'app._hash_.js', format: 'esm', assetFileNames: '[name][extname]', }, plugins: [ alias({ entries: { '@welldone-software/why-did-you-render': `${__dirname}/../src/index.js`, }, }), replace({ preventAssignment: true, values: { 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 'process.env.PORT': JSON.stringify(process.env.PORT), } }), babel({ exclude: 'node_modules/**', babelHelpers: 'bundled', }), nodeResolve({ mainFields: ['module', 'browser', 'main'], }), commonjs({}), refresh(), ], }; ================================================ FILE: demo/public/index.html ================================================ whyDidYouRender demos
================================================ FILE: demo/serve.js ================================================ const React = require('react'); const ReactDomServer = require('react-dom/server'); const express = require('express'); const fallback = require('express-history-api-fallback'); const http = require('http'); const config = require('./nollup.config.js'); const nollupDevServer = require('nollup/lib/dev-middleware'); const DemoComponent = require('./src/ssr/DemoComponent'); const port = process.env.PORT; if (!port) { throw new Error('please specify PORT in env variables.'); } const app = express(); app.get('/ssrComponent', (req, res) => { const html = ReactDomServer.renderToString( React.createElement(DemoComponent, {text: 'hydrated hi'}) ); res.send(html); }); const server = http.createServer(app); app.use(nollupDevServer(app, config, { watch: ['demo/src', 'src'], hot: true, }, server)); app.use(express.static('demo/public')); app.use(fallback('index.html', {root: 'demo/public'})); server.listen(port, () => { // eslint-disable-next-line no-console console.log(`Listening on http://localhost:${port}`); }); ================================================ FILE: demo/src/App.js ================================================ import React from 'react'; import ReactDom from 'react-dom/client'; import whyDidYouRender from '@welldone-software/why-did-you-render'; import Menu from './Menu'; import bigList from './bigList'; import propsChanges from './propsChanges'; import stateChanges from './stateChanges'; import childOfPureComponent from './childOfPureComponent'; import bothChanges from './bothChanges'; import noChanges from './noChanges'; import specialChanges from './specialChanges'; import ssr from './ssr'; import hotReload from './hotReload'; import createFactory from './createFactory'; import cloneElement from './cloneElement'; import useState from './hooks/useState'; import useContext from './hooks/useContext'; import useMemoAndCallbackChild from './hooks/useMemoAndCallbackChild'; import useReducer from './hooks/useReducer'; import reactReduxHOC from './reactReduxHOC'; import strict from './strict'; import reactRedux from './reactRedux'; import styledComponents from './styledComponents'; import logOwnerReasons from './logOwnerReasons'; import forwardRef from './forwardRef'; const demosList = { bigList, propsChanges, stateChanges, childOfPureComponent, bothChanges, noChanges, specialChanges, ssr, hotReload, createFactory, cloneElement, useState, useContext, useMemoAndCallbackChild, useReducer, strict, reactRedux, reactReduxHOC, styledComponents, logOwnerReasons, forwardRef, }; const defaultDemoName = 'bigList'; const domElement = document.getElementById('demo'); let reactDomRoot; function changeDemo(demoFn, {shouldCreateRoot = true} = {}) { console.clear && console.clear(); // eslint-disable-line no-console React.__REVERT_WHY_DID_YOU_RENDER__ && React.__REVERT_WHY_DID_YOU_RENDER__(); reactDomRoot?.unmount(); if (shouldCreateRoot) { reactDomRoot = ReactDom.createRoot(domElement); } setTimeout(() => { const reactDomRootPromise = demoFn({whyDidYouRender, domElement, reactDomRoot}); if (reactDomRootPromise) { reactDomRootPromise.then(r => reactDomRoot = r); } }, 1); } const demoFromHash = demosList[window.location.hash.substr(1)]; const initialDemo = demoFromHash || demosList[defaultDemoName]; if (!demoFromHash) { window.location.hash = defaultDemoName; } changeDemo(initialDemo.fn, initialDemo.settings); const DemoLink = ({name, description, fn, settings}) => (
  • changeDemo(fn, settings)}>{description}
  • ); const App = () => ( { Object .entries(demosList) .map(([demoName, demoData]) => ) } ); export default App; ================================================ FILE: demo/src/Menu.js ================================================ import React from 'react'; let Menu = ({children}) => (

    whyDidYouRender Demos

     Open the console   and click on one of the demos

      {children}
    ); export default Menu; ================================================ FILE: demo/src/bigList/index.js ================================================ import React from 'react'; import {times} from 'lodash'; export default { description: 'Big List (Main Demo)', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); class BigListPureComponent extends React.PureComponent { static whyDidYouRender = {customName: 'BigList'}; render() { return (

    BigListPureComponent

    {times(3000).map(n =>
    Element #{n}
    )}
    ); } } const bigListStyle = {width: '100%'}; // eslint-disable-line no-unused-vars // Notice, that unlike the huge list, we don't track Main's re-renders because we don't care about it's re-renders. class Main extends React.Component { state = {count: 0}; render() { return (

    Big List (Main Demo)

    {'Open the console and notice how the heavy list re-renders on every click on "Increase!" even though it\'s props are the same.'}

    Count: {this.state.count}
    {/* this is how you can prevent re-renders: */} {/* */}
    ); } } reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/bothChanges/index.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'Props And State Changes', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); class ClassDemo extends React.Component { static whyDidYouRender = true; state = { c: {d: 'd'}, }; static getDerivedStateFromProps() { return { c: {d: 'd'}, }; } render() { return
    State And Props Changes
    ; } } stepLogger('First Render'); reactDomRoot.render(); stepLogger('Second Render', true); reactDomRoot.render(); }, }; ================================================ FILE: demo/src/childOfPureComponent/index.js ================================================ import React from 'react'; export default { description: 'Child of Pure Component', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React, { trackAllPureComponents: true, }); const SomeChild = () => (
    Child!
    ); class PureFather extends React.PureComponent { render() { return (
    {this.props.children}
    ); } } class Main extends React.Component { state = {clicksCount: 0}; render() { return (
    ); } } reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/cloneElement/index.js ================================================ import React from 'react'; export default { description: 'Creating react element using React.cloneElement', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); class TestComponent extends React.Component { static whyDidYouRender = true; render() { return (
    TestComponent
    ); } } const testElement = ; const testElement2 = React.cloneElement(testElement); reactDomRoot.render(testElement); reactDomRoot.render(testElement2); }, }; ================================================ FILE: demo/src/createFactory/index.js ================================================ import React from 'react'; export default { description: 'Creating react element using React.createFactory', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); class TestComponent extends React.Component { static whyDidYouRender = true; render() { return (
    TestComponent
    ); } } const TestComponentFactory = React.createFactory(TestComponent); reactDomRoot.render(TestComponentFactory({a: 1})); reactDomRoot.render(TestComponentFactory({a: 1})); }, }; ================================================ FILE: demo/src/createStepLogger.js ================================================ const shouldTriggerComment = 'Should trigger whyDidYouRender'; const shouldNotTriggerComment = 'Shouldn\'t trigger whyDidYouRender'; export default function createStepLogger() { let step = 0; return function stepLogger(description = '', shouldTrigger) { const comment = shouldTrigger ? shouldTriggerComment : shouldNotTriggerComment; // eslint-disable-next-line no-console console.log( `\nRender #${step++} %c${description} %c${comment}`, 'color:blue', shouldTrigger ? 'color:red' : 'color:green' ); }; } ================================================ FILE: demo/src/forwardRef/index.js ================================================ import React from 'react'; export default { description: 'forwardRef', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); const Main = React.forwardRef((props, ref) => { return
    hi
    ; }); Main.whyDidYouRender = true; Main.displayName = 'Main'; const App = () => { const [,setState] = React.useState(0); React.useLayoutEffect(() => { setState(s => s + 1); }, []); return
    ; }; App.displayName = 'App'; reactDomRoot.render(); }, }; ================================================ FILE: demo/src/hooks/useContext.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'Hooks - useContext', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); const stepLogger = createStepLogger(); const MyContext = React.createContext({c: 'c'}); let alreadyMountedComponentWithContextHook = false; function ComponentWithContextHook() { if (alreadyMountedComponentWithContextHook) { stepLogger('renders ComponentWithContextHook with deep equal context', true); } else { alreadyMountedComponentWithContextHook = true; } const currentContext = React.useContext(MyContext); return (

    {currentContext.c}

    ); } ComponentWithContextHook.whyDidYouRender = true; let alreadyMountedComponentWithContextHookInsideMemoizedParent = false; function ComponentWithContextHookInsideMemoizedParent() { if (alreadyMountedComponentWithContextHookInsideMemoizedParent) { stepLogger('renders ComponentWithContextHookInsideMemoizedParent with deep equal context', true); } else { alreadyMountedComponentWithContextHookInsideMemoizedParent = true; } const currentContext = React.useContext(MyContext); return (

    {currentContext.c}

    ); } ComponentWithContextHookInsideMemoizedParent.whyDidYouRender = true; const MemoizedParent = React.memo(() => (
    )); MemoizedParent.displayName = 'MemoizedParent'; MemoizedParent.whyDidYouRender = true; let alreadyMountedMain = false; function Main() { const [currentState, setCurrentState] = React.useState({c: 'context value'}); if (alreadyMountedMain) { stepLogger('renders Main and it would trigger the render of ComponentWithContextHook because it\'s not pure', true); } else { alreadyMountedMain = true; } React.useLayoutEffect(() => { setCurrentState({c: 'context value'}); }, []); return (

    {`While somehow weird, we have two notifications for "ComponentWithContextHook" since it is re-rendered regardless of context changes because "Main" is re-rendered and ComponentWithContextHook is not pure`}

    ComponentWithContextHook

    MemoizedParent
    ); } stepLogger('initial render'); reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/hooks/useMemoAndCallbackChild.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'Hooks - useMemo and useCallback Child', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); const Comp = ({useMemoFn, useCallbackFn}) => { const onClick = (...args) => { useMemoFn(...args); useCallbackFn(...args); }; return
    hi!
    ; }; Comp.displayName = 'Comp'; Comp.whyDidYouRender = true; const ComponentWithNewResultsForNewDeps = React.memo(({count}) => { stepLogger('render component with always new results for new deps'); const useMemoFn = React.useMemo(() => () => 'a', [count]); const useCallbackFn = React.useCallback(() => 'a', [count]); return ( ); }); ComponentWithNewResultsForNewDeps.displayName = 'ComponentWithNewResultsForNewDeps'; const ComponentWithNewResultsForDeepEqualsDeps = React.memo(({count}) => { if (count === 0) { stepLogger('render component with always deep equals results - first render', false); } else { stepLogger('render component with always deep equals results - next render', true); } const useMemoFn = React.useMemo(() => () => 'a', [{dep1: 'dep1'}]); const useCallbackFn = React.useCallback(() => 'a', [{dep2: 'dep2'}]); return ( ); }); ComponentWithNewResultsForDeepEqualsDeps.displayName = 'ComponentWithNewResultsForDeepEqualsDeps'; function Main() { const [count, setCount] = React.useState(0); return (
    ); } Main.displayName = 'Main'; reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/hooks/useReducer.js ================================================ /* eslint-disable no-console */ import React from 'react'; export default { description: 'Hooks - useReducer', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); function reducer(state, action) { switch (action.type) { case 'broken-set-count': return {count: action.payload.count}; case 'set-count': if (action.payload.count === state.count) { return state; } return {count: action.payload.count}; } } const initialState = {count: '0'}; function Main() { const [state, dispatch] = React.useReducer(reducer, initialState); const inputRef = React.createRef(); return (

    current count: {state.count}


    ); } Main.whyDidYouRender = true; reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/hooks/useState.js ================================================ /* eslint-disable no-console */ import React from 'react'; export default { description: 'Hooks - useState', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); function BrokenHooksComponent() { console.log('render BrokenHooksComponent'); const [numObj, setNumObj] = React.useState({num: 0}); return ( <>

    {'Will cause a re-render since {num: 0} !== {num: 0}'}

    ); } BrokenHooksComponent.whyDidYouRender = true; const BrokenHooksPureComponent = React.memo(BrokenHooksComponent); BrokenHooksPureComponent.displayName = 'BrokenHooksPureComponent'; BrokenHooksPureComponent.whyDidYouRender = true; function CorrectHooksComponent() { console.log('render CorrectHooksComponent'); const [num, setNum] = React.useState(0); return ( <>

    {'Will NOT cause a re-render since 0 === 0'}

    ); } CorrectHooksComponent.whyDidYouRender = true; function useNumState(defState) { const [state, setState] = React.useState(defState); function smartSetState(newState) { if (state.num !== newState.num) { setState(newState); } } return [state, smartSetState]; } function SmartHooksComponent() { console.log('render SmartHooksComponent'); const [numObj, setNumObj] = useNumState({num: 0}); return ( <>

    {'Will NOT cause a re-render setState won\'t be called'}

    ); } SmartHooksComponent.whyDidYouRender = true; function Main() { return (
    BrokenHooksPureComponent

    BrokenHooksComponent

    CorrectHooksComponent

    SmartHooksComponent
    ); } reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/hotReload/index.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; const text = 'change me when the app is running please'; const DemoComponent = ({children}) => (

    {text}

    {children}
    ); DemoComponent.whyDidYouRender = true; export default { description: 'React Hot Reload Of Tracked Component', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); stepLogger('initial render'); reactDomRoot.render(yo!); stepLogger('render with same props', true); reactDomRoot.render(yo!); }, }; ================================================ FILE: demo/src/index.js ================================================ import React from 'react'; import ReactDom from 'react-dom/client'; import App from './App'; const element = document.getElementById('menu'); const root = ReactDom.createRoot(element); root.render(); ================================================ FILE: demo/src/logOwnerReasons/index.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'Log Owner Reasons', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); const Child = () => null; Child.whyDidYouRender = true; const Owner = () => ; class ClassOwner extends React.Component { state = {a: 1}; componentDidMount() { this.setState({a: 2}); } render() { return ; } } function HooksOwner() { /* eslint-disable no-unused-vars */ const [a, setA] = React.useState(1); const [b, setB] = React.useState(1); /* eslint-enable */ React.useEffect(() => { setA(2); setB(2); }, []); return ; } stepLogger('First render'); reactDomRoot.render(); stepLogger('Owner props change', true); reactDomRoot.render(); stepLogger('Owner state change', true); reactDomRoot.render(); stepLogger('Owner hooks changes', true); reactDomRoot.render(); }, }; ================================================ FILE: demo/src/noChanges/index.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'No Changes', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); class ClassDemo extends React.Component { static whyDidYouRender = true; componentDidMount() { stepLogger('forceUpdate', true); this.forceUpdate(); } render() { return
    State And Props The Same
    ; } } stepLogger('First Render'); reactDomRoot.render(); }, }; ================================================ FILE: demo/src/propsChanges/index.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'Props Changes', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); const ClassDemo = () => (
    Props Changes
    ); ClassDemo.whyDidYouRender = true; const Main = props => ( ); stepLogger('First render'); reactDomRoot.render(
    ); stepLogger('Same props', true); reactDomRoot.render(
    ); stepLogger('Other props'); reactDomRoot.render(
    ); stepLogger('Different by ref, equals by value', true); reactDomRoot.render(
    ); stepLogger('Other nested props'); reactDomRoot.render(
    ); stepLogger('Deep equal nested props', true); reactDomRoot.render(
    ); stepLogger('Mixed Props'); reactDomRoot.render(
    ); stepLogger('Mixed Props again', true); reactDomRoot.render(
    ); const sameObj = {a: {b: 'c'}}; stepLogger('Mixed Props including eq obj'); reactDomRoot.render(
    ); stepLogger('Mixed Props including eq obj', true); reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/reactRedux/index.js ================================================ import React from 'react'; import _ from 'lodash'; import {createStore} from 'redux'; import * as Redux from 'react-redux'; export default { description: 'React Redux', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React, {trackExtraHooks: [ [Redux, 'useSelector'], ]}); const useDispatch = Redux.useDispatch; const useSelector = Redux.useSelector; const Provider = Redux.Provider; const ConnectedSimpleComponent = () => { const a = useSelector(state => state.a); const dispatch = useDispatch(); return (
    {`{a.b} is: ${a.b}`}
    ); }; ConnectedSimpleComponent.whyDidYouRender = true; const initialState = {a: {b: 'c'}}; const store = createStore((state = initialState, action) => { if (action.type === 'randomObj') { return {a: {b: `${Math.random()}`}}; } if (action.type === 'deepEqlObj') { return _.cloneDeep(state); } return state; }); const Main = () => ( ); reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/reactReduxHOC/index.js ================================================ import React from 'react'; import {createStore} from 'redux'; import * as Redux from 'react-redux'; import _ from 'lodash'; const connect = Redux.connect; const Provider = Redux.Provider; export default { description: 'React Redux HOC', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); const initialState = {a: {b: 'c'}}; const rootReducer = (state, action) => { if (action.type === 'randomObj') { return {a: {b: `${Math.random()}`}}; } if (action.type === 'deepEqlObj') { return _.cloneDeep(state); } return state; }; const store = createStore(rootReducer, initialState); const SimpleComponent = ({a, randomObj, deepEqlObj, sameObj}) => { return (
    {`{a.b} is: ${a.b}`}
    ); }; const ConnectedSimpleComponent = connect( state => ({a: state.a}), ({ randomObj: () => ({type: 'randomObj'}), deepEqlObj: () => ({type: 'deepEqlObj'}), sameObj: () => ({type: 'sameObj'}), }) )(SimpleComponent); SimpleComponent.whyDidYouRender = true; const Main = () => ( ); reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/specialChanges/index.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'Special Changes', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); class ClassDemo extends React.Component { static whyDidYouRender = true; render() { return
    Special Changes
    ; } } stepLogger('First render'); reactDomRoot.render( hi!} /> ); stepLogger('Same special props', true); reactDomRoot.render( hi!} /> ); }, }; ================================================ FILE: demo/src/ssr/DemoComponent.js ================================================ const React = require('react'); const createReactClass = require('create-react-class'); const DemoComponent = createReactClass({ displayName: 'DemoComponent', render() { return React.createElement('div', {}, this.props.text); }, }); DemoComponent.whyDidYouRender = true; module.exports = DemoComponent; ================================================ FILE: demo/src/ssr/index.js ================================================ import React from 'react'; import ReactDom from 'react-dom/client'; import createStepLogger from '../createStepLogger'; import DemoComponent from './DemoComponent'; export default { description: 'Server Side (hydrate)', fn({domElement, whyDidYouRender}) { const stepLogger = createStepLogger(); return fetch('/ssrComponent') .then(response => response.text()) .then(initialDemoHTML => { domElement.innerHTML = initialDemoHTML; whyDidYouRender(React); stepLogger('hydrate'); const hydratedRoot = ReactDom.hydrateRoot(domElement, ); setTimeout(() => { stepLogger('render with same props', true); hydratedRoot.render(); }, 1); return hydratedRoot; }); }, settings: {shouldCreateRoot: false}, }; ================================================ FILE: demo/src/stateChanges/index.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'State Changes', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); class ClassDemo extends React.Component { static whyDidYouRender = true; state = { stateKey: 'stateValue', }; componentDidMount() { stepLogger('Set an existing state key with the same value', true); this.setState({stateKey: 'stateValue'}, () => { stepLogger('Add object entry'); this.setState({objectKey: {a: 'a'}}, () => { stepLogger('Add a new object entry that equals by value', true); this.setState({objectKey: {a: 'a'}}); }); }); } render() { return
    State Changes
    ; } } stepLogger('First Render'); reactDomRoot.render(); }, }; ================================================ FILE: demo/src/strict/index.js ================================================ import React from 'react'; import createStepLogger from '../createStepLogger'; export default { description: 'Strict mode', fn({reactDomRoot, whyDidYouRender}) { const stepLogger = createStepLogger(); whyDidYouRender(React); class ClassDemo extends React.Component { static whyDidYouRender = true; render() { return
    Props Changes
    ; } } const Main = props => ( ); stepLogger('First render'); reactDomRoot.render(
    ); stepLogger('Same props', true); reactDomRoot.render(
    ); stepLogger('Other props'); reactDomRoot.render(
    ); stepLogger('Different by ref, equals by value', true); reactDomRoot.render(
    ); stepLogger('Other nested props'); reactDomRoot.render(
    ); stepLogger('Deep equal nested props', true); reactDomRoot.render(
    ); }, }; ================================================ FILE: demo/src/styledComponents/index.js ================================================ import React from 'react'; import styled from 'styled-components'; export default { description: 'styled-components', fn({reactDomRoot, whyDidYouRender}) { whyDidYouRender(React); const SimpleComponent = (props) => { return (
    styled-components
    ); }; const StyledSimpleComponent = styled(SimpleComponent)` background-color: #ff96ae; font-style: italic; `; StyledSimpleComponent.whyDidYouRender = true; const Main = () => ( ); reactDomRoot.render(
    ); reactDomRoot.render(
    ); }, }; ================================================ FILE: eslint.config.js ================================================ const reactPlugin = require('eslint-plugin-react'); const js = require('@eslint/js'); const globals = require('globals'); const {includeIgnoreFile} = require('@eslint/compat'); const pluginCypress = require('eslint-plugin-cypress/flat'); // TODO: remove once all deps are using the latest version globals.browser['AudioWorkletGlobalScope'] = globals.browser['AudioWorkletGlobalScope ']; delete globals.browser['AudioWorkletGlobalScope ']; module.exports = [ includeIgnoreFile(__dirname + '/.gitignore'), js.configs.recommended, pluginCypress.configs.globals, { plugins: { cypress: pluginCypress }, rules: { 'cypress/unsafe-to-chain-command': 'error' }, }, { ...reactPlugin.configs.flat.recommended, languageOptions: { ...reactPlugin.configs.flat.recommended.languageOptions, globals: { ...globals.browser, ...globals.jest, ...globals.node, ...globals.console, flushConsoleOutput: 'readable', }, }, rules: { 'semi': ['error', 'always'], 'curly': 'error', 'no-var': 'error', 'quotes': ['error', 'single'], 'no-console': 'error', 'no-debugger': 'warn', 'react/jsx-uses-vars': 'error', 'react/jsx-uses-react': 'error', 'no-unused-vars': ['error', { 'ignoreRestSiblings': true, 'varsIgnorePattern': '^_', 'argsIgnorePattern': '^_', 'caughtErrorsIgnorePattern': '^_', 'destructuredArrayIgnorePattern': '^_' }], 'eol-last': 'error', 'object-curly-spacing': ['error', 'never'], 'react/prop-types': 'off', 'react/display-name': 'off', 'space-before-function-paren': ['error', 'never'], 'space-before-blocks': ['error', 'always'], 'space-in-parens': ['error', 'never'], 'comma-dangle': ['error', 'only-multiline'], 'func-call-spacing': ['error', 'never'], 'no-multi-spaces': 'error', 'indent': ['error', 2] } } ]; ================================================ FILE: jest.config.js ================================================ module.exports = { cacheDirectory: '.cache/jest-cache', setupFiles: ['./jest.polyfills.js'], setupFilesAfterEnv: [ '/jestSetup.js', ], moduleNameMapper: { '~(.*)$': '/src$1', '^@welldone-software/why-did-you-render$': '/src/whyDidYouRender.js', }, testEnvironment: 'jsdom', extensionsToTreatAsEsm: ['.ts', '.tsx'], }; ================================================ FILE: jest.polyfills.js ================================================ // jest.polyfills.js /** * @note The block below contains polyfills for Node.js globals * required for Jest to function when running JSDOM tests. * These HAVE to be require's and HAVE to be in this exact * order, since "undici" depends on the "TextEncoder" global API. * * Consider migrating to a more modern test runner if * you don't want to deal with this. */ const {TextDecoder, TextEncoder} = require('node:util'); Object.defineProperties(globalThis, { TextDecoder: {value: TextDecoder}, TextEncoder: {value: TextEncoder}, }); const {Blob, File} = require('node:buffer'); Object.defineProperties(globalThis, { Blob: {value: Blob}, File: {value: File}, }); window.MessageChannel = jest.fn().mockImplementation(() => { let onmessage; return { port1: { set onmessage(cb) { onmessage = cb; }, }, port2: { postMessage: data => { if (onmessage) { onmessage({data}); } }, }, }; }); ================================================ FILE: jestSetup.js ================================================ import {errorOnConsoleOutput} from '@welldone-software/jest-console-handler'; const substringsToIgnore = [ 'Selectors that return the entire state are almost certainly a mistake', 'Warning: ReactDOM.render is no longer supported in React 19', 'Support for defaultProps will be removed from', ]; const regexToIgnore = new RegExp(`(${substringsToIgnore.join('|')})`); global.flushConsoleOutput = errorOnConsoleOutput({filterEntries: ({args}) => { const shouldIgnoreConsoleLog = regexToIgnore.test(args[0]); return !shouldIgnoreConsoleLog; }}); const React = require('react'); if (!React.version.startsWith('19')) { throw new Error(`Wrong React version. Expected ^19, got ${React.version}`); } ================================================ FILE: jsx-dev-runtime.d.ts ================================================ import './types.d.ts'; ================================================ FILE: jsx-dev-runtime.js ================================================ /* eslint-disable*/ var jsxDevRuntime = require('react/jsx-dev-runtime') var WDYR = require('@welldone-software/why-did-you-render') var origJsxDev = jsxDevRuntime.jsxDEV var wdyrStore = WDYR.wdyrStore module.exports = { ...jsxDevRuntime, jsxDEV(...args) { if (wdyrStore.React && wdyrStore.React.__IS_WDYR__) { var origType = args[0] var rest = args.slice(1) var WDYRType = WDYR.getWDYRType(origType) if (WDYRType) { try { wdyrStore.ownerBeforeElementCreation = WDYR.getCurrentOwner(); var element = origJsxDev.apply(null, [WDYRType].concat(rest)) if (wdyrStore.options.logOwnerReasons) { WDYR.storeOwnerData(element) } return element } catch(e) { wdyrStore.options.consoleLog('whyDidYouRender JSX transform error. Please file a bug at https://github.com/welldone-software/why-did-you-render/issues.', { errorInfo: { error: e, componentNameOrComponent: origType, rest: rest, options: wdyrStore.options } }) } } } return origJsxDev.apply(null, args) } }; ================================================ FILE: jsx-runtime.d.ts ================================================ import './types.d.ts'; ================================================ FILE: jsx-runtime.js ================================================ module.exports = require('react/jsx-runtime'); ================================================ FILE: package.json ================================================ { "name": "@welldone-software/why-did-you-render", "description": "Monkey patches React to notify you about avoidable re-renders.", "version": "10.0.1", "repository": "git+https://github.com/welldone-software/why-did-you-render.git", "license": "MIT", "authors": [ "Vitali Zaidman (https://github.com/vzaidman)" ], "types": "types.d.ts", "main": "dist/whyDidYouRender.js", "files": [ "dist", "types.d.ts", "jsx-runtime.js", "jsx-runtime.d.ts", "jsx-dev-runtime.js", "jsx-dev-runtime.d.ts" ], "keywords": [ "react", "component", "pure", "performance", "render", "update", "tool" ], "scripts": { "start": "cross-env PORT=3003 NODE_ENV=development node demo/serve", "build": "cross-env NODE_ENV=production rollup --config --bundleConfigAsCjs", "test": "jest --config=jest.config.js", "test:ci": "yarn test --coverage", "lint": "eslint . --max-warnings 0 --cache --cache-location .cache/eslint-cache", "clear": "rimraf .cache dist demo/dist node_modules", "watch": "concurrently --names \"Serve,Test\" \"npm:start\" \"npm:test:watch\"", "checkHealth": "yarn build && yarn lint && yarn test && yarn cypress:test", "version": "yarn checkHealth", "postversion": "git push && git push --tags", "cypress:open": "cypress open", "cypress:run": "cypress run --browser chrome", "cypress:test": "start-server-and-test start http://localhost:3003 cypress:run" }, "comments": { "how to": { "bump version": "npm version major/minor/patch" }, "resolutions": { "source-map@^0.7.4": [ "fixes https://github.com/mozilla/source-map/issues/432 or we get:", "forces nollup to use source-map 0.8.0-beta.0 or higher.", "will be resolved when nollup is updated to use it" ], "rollup-plugin-react-refresh": [ "Uses my forked github https://github.com/vzaidman/rollup-plugin-react-refresh", "Until the PR from that is merged to the official library https://github.com/PepsRyuu/rollup-plugin-react-refresh/pull/10" ] } }, "dependencies": { "lodash": "^4" }, "peerDependencies": { "react": "^19" }, "resolutions": { "source-map-fast": "npm:source-map@^0.8.0-beta.0", "source-map": "^0.8.0-beta.0" }, "devDependencies": { "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "@babel/preset-react": "^7.26.3", "@eslint/compat": "^1.2.4", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^28.0.2", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-replace": "^6.0.2", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.1.0", "@types/jest": "^29.5.14", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@types/react-redux": "^7.1.34", "@welldone-software/jest-console-handler": "^2.0.1", "acorn-walk": "^8.3.4", "astring": "^1.9.0", "babel-core": "^7.0.0-bridge.0", "babel-jest": "^29.7.0", "concurrently": "^9.1.1", "create-react-class": "^15.7.0", "cross-env": "^7.0.3", "cypress": "^13.17.0", "eslint": "^9.17.0", "eslint-plugin-cypress": "^4.1.0", "eslint-plugin-jest": "^28.10.0", "eslint-plugin-react": "^7.37.3", "express": "^4.21.2", "express-history-api-fallback": "^2.2.1", "husky": "^9.1.7", "jest": "^29.7.0", "jest-cli": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "nollup": "^0.21.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-is": "^19.0.0", "react-redux": "^9.2.0", "react-refresh": "^0.16.0", "react-router-dom": "^7.1.1", "redux": "^5.0.1", "rimraf": "^6.0.1", "rollup": "^4.29.1", "rollup-plugin-alias": "^2.2.0", "rollup-plugin-commonjs-alternate": "^0.8.0", "rollup-plugin-license": "^3.5.3", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-react-refresh": "https://github.com/vzaidman/rollup-plugin-react-refresh.git#5c2f09bc28dbb8ab711b7d095f61fbc8d295fcd6", "source-map": "npm:source-map@^0.8.0-beta.0", "start-server-and-test": "^2.0.9", "styled-components": "^6.1.13", "typescript": "^5.7.2" } } ================================================ FILE: rollup.config.js ================================================ import fs from 'fs'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import babel from '@rollup/plugin-babel'; import license from 'rollup-plugin-license'; const loadJSON = (path) => JSON.parse(fs.readFileSync(new URL(path, import.meta.url))); const pkg = loadJSON('./package.json'); const banner = ` <%= pkg.name %> <%= pkg.version %> MIT Licensed Generated by <%= pkg.authors[0] %> Generated at <%= moment().format('YYYY-MM-DD') %> `; export default [ { input: 'src/index.js', external: ['lodash', 'react'], output: [ { name: 'whyDidYouRender', file: pkg.main, format: 'umd', sourcemap: true, exports: 'default', globals: { lodash: 'lodash', react: 'react', }, }, ], plugins: [ babel({ exclude: 'node_modules/**', babelHelpers: 'bundled', }), resolve(), commonjs(), license({ sourcemap: true, banner, }), ], }, ]; ================================================ FILE: src/calculateDeepEqualDiffs.js ================================================ import { isArray, isPlainObject, isDate, isRegExp, isError, isFunction, isSet, has, uniq, } from 'lodash'; import {diffTypes} from './consts'; const hasElementType = typeof Element !== 'undefined'; // copied from https://github.com/facebook/react/blob/fc5ef50da8e975a569622d477f1fed54cb8b193d/packages/react-devtools-shared/src/backend/shared/ReactSymbols.js#L26 const hasSymbol = typeof Symbol === 'function' && Symbol.for; const LEGACY_ELEMENT_NUMBER = 0xeac7; const LEGACY_ELEMENT_SYMBOL_STRING = hasSymbol && Symbol.for('react.element'); const ELEMENT_SYMBOL_STRING = hasSymbol && Symbol.for('react.transitional.element'); const isReactElement = object => [ ...(hasSymbol ? [ELEMENT_SYMBOL_STRING, LEGACY_ELEMENT_SYMBOL_STRING] : []), LEGACY_ELEMENT_NUMBER, ].includes(object.$$typeof); // end function trackDiff(a, b, diffsAccumulator, pathString, diffType) { diffsAccumulator.push({ diffType, pathString, prevValue: a, nextValue: b, }); return diffType !== diffTypes.different; } function isGetter(obj, prop) { return !!Object.getOwnPropertyDescriptor(obj, prop)['get']; } export const dependenciesMap = new WeakMap(); function accumulateDeepEqualDiffs(a, b, diffsAccumulator, pathString = '', {detailed}) { if (a === b) { if (detailed) { trackDiff(a, b, diffsAccumulator, pathString, diffTypes.same); } return true; } if (!a || !b) { return trackDiff(a, b, diffsAccumulator, pathString, diffTypes.different); } if (isArray(a) && isArray(b)) { const arrayLength = a.length; if (arrayLength !== b.length) { return trackDiff([...a], [...b], diffsAccumulator, pathString, diffTypes.different); } const arrayItemDiffs = []; let numberOfDeepEqualsItems = 0; for (let i = arrayLength; i--; i > 0) { const diffEquals = accumulateDeepEqualDiffs(a[i], b[i], arrayItemDiffs, `${pathString}[${i}]`, {detailed}); if (diffEquals) { numberOfDeepEqualsItems++; } } if (detailed || numberOfDeepEqualsItems !== arrayLength) { diffsAccumulator.push(...arrayItemDiffs); } if (numberOfDeepEqualsItems === arrayLength) { return trackDiff([...a], [...b], diffsAccumulator, pathString, diffTypes.deepEquals); } return trackDiff([...a], [...b], diffsAccumulator, pathString, diffTypes.different); } if (isSet(a) && isSet(b)) { if (a.size !== b.size) { return trackDiff(new Set(a), new Set(b), diffsAccumulator, pathString, diffTypes.different); } for (const valA of a) { if (!b.has(valA)) { return trackDiff(new Set(a), new Set(b), diffsAccumulator, pathString, diffTypes.different); } } return trackDiff(new Set(a), new Set(b), diffsAccumulator, pathString, diffTypes.deepEquals); } if (isDate(a) && isDate(b)) { return a.getTime() === b.getTime() ? trackDiff(new Date(a), new Date(b), diffsAccumulator, pathString, diffTypes.date) : trackDiff(new Date(a), new Date(b), diffsAccumulator, pathString, diffTypes.different); } if (isRegExp(a) && isRegExp(b)) { return a.toString() === b.toString() ? trackDiff(a, b, diffsAccumulator, pathString, diffTypes.regex) : trackDiff(a, b, diffsAccumulator, pathString, diffTypes.different); } if (hasElementType && a instanceof Element && b instanceof Element) { return trackDiff(a, b, diffsAccumulator, pathString, diffTypes.different); } if (isReactElement(a) && isReactElement(b)) { if (a.type !== b.type) { return trackDiff(a, b, diffsAccumulator, pathString, diffTypes.different); } const reactElementPropsAreDeepEqual = accumulateDeepEqualDiffs(a.props, b.props, [], `${pathString}.props`, {detailed}); return reactElementPropsAreDeepEqual ? trackDiff(a, b, diffsAccumulator, pathString, diffTypes.reactElement) : trackDiff(a, b, diffsAccumulator, pathString, diffTypes.different); } if (isFunction(a) && isFunction(b)) { if (a.name !== b.name) { return trackDiff(a, b, diffsAccumulator, pathString, diffTypes.different); } const aDependenciesObj = dependenciesMap.get(a); const bDependenciesObj = dependenciesMap.get(b); if (aDependenciesObj && bDependenciesObj) { const dependenciesAreDeepEqual = accumulateDeepEqualDiffs(aDependenciesObj.deps, bDependenciesObj.deps, diffsAccumulator, `${pathString}:parent-hook-${aDependenciesObj.hookName}-deps`, {detailed}); return dependenciesAreDeepEqual ? trackDiff(a, b, diffsAccumulator, pathString, diffTypes.function) : trackDiff(a, b, diffsAccumulator, pathString, diffTypes.different); } return trackDiff(a, b, diffsAccumulator, pathString, diffTypes.function); } if (typeof a === 'object' && typeof b === 'object' && Object.getPrototypeOf(a) === Object.getPrototypeOf(b)) { const aKeys = Object.getOwnPropertyNames(a); const bKeys = Object.getOwnPropertyNames(b); const allKeys = uniq([...aKeys, ...bKeys]); const clonedA = isPlainObject(a) ? {...a} : a; const clonedB = isPlainObject(b) ? {...b} : b; if (allKeys.length !== aKeys.length || allKeys.length !== bKeys.length) { return trackDiff(clonedA, clonedB, diffsAccumulator, pathString, diffTypes.different); } const relevantKeys = allKeys.filter(key => { // do not compare the stack as it differ even though the errors are identical. if (key === 'stack' && isError(a)) { return false; } // getters checking is causing too much problems because of how it's used in js. // not only getters can throw errors, they also cause side effects in many cases. if (isGetter(a, key)) { return false; } return true; }); const keysLength = relevantKeys.length; for (let i = keysLength; i--; i > 0) { if (!has(b, relevantKeys[i])) { return trackDiff(clonedA, clonedB, diffsAccumulator, pathString, diffTypes.different); } } const objectValuesDiffs = []; let numberOfDeepEqualsObjectValues = 0; for (let i = keysLength; i--; i > 0) { const key = relevantKeys[i]; const deepEquals = accumulateDeepEqualDiffs(a[key], b[key], objectValuesDiffs, `${pathString}.${key}`, {detailed}); if (deepEquals) { numberOfDeepEqualsObjectValues++; } } if (detailed || numberOfDeepEqualsObjectValues !== keysLength) { diffsAccumulator.push(...objectValuesDiffs); } if (numberOfDeepEqualsObjectValues === keysLength) { return trackDiff(clonedA, clonedB, diffsAccumulator, pathString, diffTypes.deepEquals); } return trackDiff(clonedA, clonedB, diffsAccumulator, pathString, diffTypes.different); } return trackDiff(a, b, diffsAccumulator, pathString, diffTypes.different); } export default function calculateDeepEqualDiffs(a, b, initialPathString, {detailed = false} = {}) { try { const diffs = []; accumulateDeepEqualDiffs(a, b, diffs, initialPathString, {detailed}); return diffs; } catch (error) { if ((error.message && error.message.match(/stack|recursion/i)) || (error.number === -2146828260)) { // warn on circular references, don't crash. // browsers throw different errors name and messages: // chrome/safari: "RangeError", "Maximum call stack size exceeded" // firefox: "InternalError", too much recursion" // edge: "Error", "Out of stack space" // eslint-disable-next-line no-console console.warn('Warning: why-did-you-render couldn\'t handle circular references in props.', error.name, error.message); return false; } throw error; } } ================================================ FILE: src/consts.js ================================================ export const diffTypes = { 'different': 'different', 'deepEquals': 'deepEquals', 'date': 'date', 'regex': 'regex', 'reactElement': 'reactElement', 'function': 'function', 'same': 'same', }; export const diffTypesDescriptions = { [diffTypes.different]: 'different objects', [diffTypes.deepEquals]: 'different objects that are equal by value', [diffTypes.date]: 'different date objects with the same value', [diffTypes.regex]: 'different regular expressions with the same value', [diffTypes.reactElement]: 'different React elements (remember that the syntax always produces a *NEW* immutable React element so a component that receives as props always re-renders)', [diffTypes.function]: 'different functions with the same name', [diffTypes.same]: 'same objects by ref (===)', }; // copied from packages/shared/ReactSymbols.js in https://github.com/facebook/react const hasSymbol = typeof Symbol === 'function' && Symbol.for; export const REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3; export const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0; export const REACT_STRICT_MODE = 0b1000; ================================================ FILE: src/defaultNotifier.js ================================================ import wdyrStore from './wdyrStore'; import {diffTypes, diffTypesDescriptions} from './consts'; import printDiff from './printDiff'; const moreInfoUrl = 'http://bit.ly/wdyr02'; const moreInfoHooksUrl = 'http://bit.ly/wdyr3'; let inHotReload = false; function shouldLog(reason, Component) { if (inHotReload) { return false; } if (wdyrStore.options.logOnDifferentValues) { return true; } if (Component.whyDidYouRender && Component.whyDidYouRender.logOnDifferentValues) { return true; } const hasDifferentValues = (( reason.propsDifferences && reason.propsDifferences.some(diff => diff.diffType === diffTypes.different) ) || ( reason.stateDifferences && reason.stateDifferences.some(diff => diff.diffType === diffTypes.different) ) || ( reason.hookDifferences && reason.hookDifferences.some(diff => diff.diffType === diffTypes.different) )); return !hasDifferentValues; } function logDifference({Component, displayName, hookName, prefixMessage, diffObjType, differences, values}) { if (differences && differences.length > 0) { wdyrStore.options.consoleLog({[displayName]: Component}, `${prefixMessage} of ${diffObjType} changes:`); differences.forEach(({pathString, diffType, prevValue, nextValue}) => { function diffFn() { printDiff(prevValue, nextValue, {pathString, consoleLog: wdyrStore.options.consoleLog}); } wdyrStore.options.consoleGroup( `%c${diffObjType === 'hook' ? `[hook ${hookName} result]` : `${diffObjType}.`}%c${pathString}%c`, `background-color: ${wdyrStore.options.textBackgroundColor};color:${wdyrStore.options.diffNameColor};`, `background-color: ${wdyrStore.options.textBackgroundColor};color:${wdyrStore.options.diffPathColor};`, 'background-color: ${wdyrStore.options.textBackgroundColor};color:default;' ); wdyrStore.options.consoleLog( `${diffTypesDescriptions[diffType]}. (more info at ${hookName ? moreInfoHooksUrl : moreInfoUrl})`, ); wdyrStore.options.consoleLog({[`prev ${pathString}`]: prevValue}, '!==', {[`next ${pathString}`]: nextValue}); if (diffType === diffTypes.deepEquals) { wdyrStore.options.consoleLog({'For detailed diff, right click the following fn, save as global, and run: ': diffFn}); } wdyrStore.options.consoleGroupEnd(); }); } else if (differences) { wdyrStore.options.consoleLog( {[displayName]: Component}, `${prefixMessage} the ${diffObjType} object itself changed but its values are all equal.`, diffObjType === 'props' ? 'This could have been avoided by making the component pure, or by preventing its father from re-rendering.' : 'This usually means this component called setState when no changes in its state actually occurred.', `More info at ${moreInfoUrl}` ); wdyrStore.options.consoleLog(`prev ${diffObjType}:`, values.prev, ' !== ', values.next, `:next ${diffObjType}`); } } export default function defaultNotifier(updateInfo) { const {Component, displayName, hookName, prevOwner, nextOwner, prevProps, prevState, prevHookResult, nextProps, nextState, nextHookResult, reason} = updateInfo; if (!shouldLog(reason, Component, wdyrStore.options)) { return; } wdyrStore.options.consoleGroup(`%c${displayName}`, `background-color: ${wdyrStore.options.textBackgroundColor};color: ${wdyrStore.options.titleColor};`); let prefixMessage = 'Re-rendered because'; if (reason.propsDifferences) { logDifference({ Component, displayName, prefixMessage, diffObjType: 'props', differences: reason.propsDifferences, values: {prev: prevProps, next: nextProps}, }); prefixMessage = 'And because'; } if (reason.stateDifferences) { logDifference({ Component, displayName, prefixMessage, diffObjType: 'state', differences: reason.stateDifferences, values: {prev: prevState, next: nextState}, }); } if (reason.hookDifferences) { logDifference({ Component, displayName, prefixMessage, diffObjType: 'hook', differences: reason.hookDifferences, values: {prev: prevHookResult, next: nextHookResult}, hookName, }); } if (reason.propsDifferences && reason.ownerDifferences) { const prevOwnerData = wdyrStore.ownerDataMap.get(prevOwner); const nextOwnerData = wdyrStore.ownerDataMap.get(nextOwner); if (prevOwnerData && nextOwnerData) { wdyrStore.options.consoleGroup(`Rendered by ${nextOwnerData.displayName}`); let prefixMessage = 'Re-rendered because'; if (reason.ownerDifferences.propsDifferences) { logDifference({ Component: nextOwnerData.Component, displayName: nextOwnerData.displayName, prefixMessage, diffObjType: 'props', differences: reason.ownerDifferences.propsDifferences, values: {prev: prevOwnerData.props, next: nextOwnerData.props}, }); prefixMessage = 'And because'; } if (reason.ownerDifferences.stateDifferences) { logDifference({ Component: nextOwnerData.Component, displayName: nextOwnerData.displayName, prefixMessage, diffObjType: 'state', differences: reason.ownerDifferences.stateDifferences, values: {prev: prevOwnerData.state, next: nextOwnerData.state}, }); } if (reason.ownerDifferences.hookDifferences) { reason.ownerDifferences.hookDifferences.forEach(({hookName, differences}, i) => logDifference({ Component: nextOwnerData.Component, displayName: nextOwnerData.displayName, prefixMessage, diffObjType: 'hook', differences, values: {prev: prevOwnerData.hooksInfo[i].result, next: nextOwnerData.hooksInfo[i].result}, hookName, }) ); } wdyrStore.options.consoleGroupEnd(); } } if (!reason.propsDifferences && !reason.stateDifferences && !reason.hookDifferences) { wdyrStore.options.consoleLog( {[displayName]: Component}, 'Re-rendered although props and state objects are the same.', 'This usually means there was a call to this.forceUpdate() inside the component.', `more info at ${moreInfoUrl}` ); } wdyrStore.options.consoleGroupEnd(); } export function createDefaultNotifier(hotReloadBufferMs) { if (hotReloadBufferMs) { if (typeof(module) !== 'undefined' && module.hot && module.hot.addStatusHandler) { module.hot.addStatusHandler(status => { if (status === 'idle') { inHotReload = true; setTimeout(() => { inHotReload = false; }, hotReloadBufferMs); } }); } } return defaultNotifier; } ================================================ FILE: src/findObjectsDifferences.js ================================================ import {reduce} from 'lodash'; import calculateDeepEqualDiffs from './calculateDeepEqualDiffs'; const emptyObject = {}; export default function findObjectsDifferences(userPrevObj, userNextObj, {shallow = true} = {}) { if (userPrevObj === userNextObj) { return false; } if (!shallow) { return calculateDeepEqualDiffs(userPrevObj, userNextObj); } const prevObj = userPrevObj || emptyObject; const nextObj = userNextObj || emptyObject; const keysOfBothObjects = Object.keys({...prevObj, ...nextObj}); return reduce(keysOfBothObjects, (result, key) => { const deepEqualDiffs = calculateDeepEqualDiffs(prevObj[key], nextObj[key], key); if (deepEqualDiffs) { result = [ ...result, ...deepEqualDiffs, ]; } return result; }, []); } ================================================ FILE: src/getDefaultProps.js ================================================ export default function getDefaultProps(type) { return ( type.defaultProps || (type.type && getDefaultProps(type.type)) || (type.render && getDefaultProps(type.render)) || undefined ); } ================================================ FILE: src/getDisplayName.js ================================================ import {isString} from 'lodash'; export default function getDisplayName(type) { return ( type.displayName || type.name || (type.type && getDisplayName(type.type)) || (type.render && getDisplayName(type.render)) || (isString(type) ? type : 'Unknown') ); } ================================================ FILE: src/getUpdateInfo.js ================================================ import findObjectsDifferences from './findObjectsDifferences'; import wdyrStore from './wdyrStore'; function getOwnerDifferences(prevOwner, nextOwner) { if (!prevOwner || !nextOwner) { return false; } const prevOwnerData = wdyrStore.ownerDataMap.get(prevOwner); const nextOwnerData = wdyrStore.ownerDataMap.get(nextOwner); if (!prevOwnerData || !nextOwnerData) { return false; } try { // in strict mode a re-render happens twice as opposed to the initial render that happens once. const prevOwnerDataHooks = prevOwnerData.hooksInfo.length === nextOwnerData.hooksInfo.length * 2 ? prevOwnerData.hooksInfo.slice(prevOwnerData.hooksInfo.length / 2) : prevOwnerData.hooksInfo; const hookDifferences = prevOwnerDataHooks.map(({hookName, result}, i) => ({ hookName, differences: findObjectsDifferences(result, nextOwnerData.hooksInfo[i].result, {shallow: false}), })); return { propsDifferences: findObjectsDifferences(prevOwnerData.props, nextOwnerData.props), stateDifferences: findObjectsDifferences(prevOwnerData.state, nextOwnerData.state), hookDifferences: hookDifferences.length > 0 ? hookDifferences : false, }; } catch(e) { wdyrStore.options.consoleLog('whyDidYouRender error in getOwnerDifferences. Please file a bug at https://github.com/welldone-software/why-did-you-render/issues.', { errorInfo: { error: e, prevOwner, nextOwner, options: wdyrStore.options, }, }); return false; } } function getUpdateReason(prevOwner, prevProps, prevState, prevHookResult, nextOwner, nextProps, nextState, nextHookResult) { return { propsDifferences: findObjectsDifferences(prevProps, nextProps), stateDifferences: findObjectsDifferences(prevState, nextState), hookDifferences: findObjectsDifferences(prevHookResult, nextHookResult, {shallow: false}), ownerDifferences: getOwnerDifferences(prevOwner, nextOwner), }; } export default function getUpdateInfo({Component, displayName, hookName, prevOwner, nextOwner, prevProps, prevState, prevHookResult, nextProps, nextState, nextHookResult}) { return { Component, displayName, hookName, prevOwner, prevProps, prevState, prevHookResult, nextOwner, nextProps, nextState, nextHookResult, reason: getUpdateReason(prevOwner, prevProps, prevState, prevHookResult, nextOwner, nextProps, nextState, nextHookResult), ownerDataMap: wdyrStore.ownerDataMap, }; } ================================================ FILE: src/helpers.js ================================================ import wdyrStore from './wdyrStore'; export function getCurrentOwner() { const reactSharedInternals = wdyrStore.React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE; const reactDispatcher = reactSharedInternals?.A; return reactDispatcher?.getOwner(); } ================================================ FILE: src/index.js ================================================ import * as React from 'react'; import wdyrStore from './wdyrStore'; import whyDidYouRender, {storeOwnerData, getWDYRType} from './whyDidYouRender'; import defaultNotifier from './defaultNotifier'; import {getCurrentOwner} from './helpers'; whyDidYouRender.defaultNotifier = defaultNotifier; whyDidYouRender.wdyrStore = wdyrStore; whyDidYouRender.storeOwnerData = storeOwnerData; whyDidYouRender.getWDYRType = getWDYRType; whyDidYouRender.getCurrentOwner = getCurrentOwner; Object.assign(whyDidYouRender, React); export default whyDidYouRender; ================================================ FILE: src/normalizeOptions.js ================================================ /* eslint-disable no-console */ import {createDefaultNotifier} from './defaultNotifier'; const emptyFn = () => {}; export default function normalizeOptions(userOptions = {}) { let consoleGroup = console.group; let consoleGroupEnd = console.groupEnd; if (userOptions.collapseGroups) { consoleGroup = console.groupCollapsed; } else if (userOptions.onlyLogs) { consoleGroup = console.log; consoleGroupEnd = emptyFn; } const notifier = userOptions.notifier || ( createDefaultNotifier( ('hotReloadBufferMs' in userOptions) ? userOptions.hotReloadBufferMs : 500 ) ); return { include: null, exclude: null, notifier, onlyLogs: false, consoleLog: console.log, consoleGroup, consoleGroupEnd, logOnDifferentValues: false, logOwnerReasons: true, trackHooks: true, titleColor: '#058', diffNameColor: 'blue', diffPathColor: 'red', textBackgroundColor: 'white', trackExtraHooks: [], trackAllPureComponents: false, ...userOptions, }; } ================================================ FILE: src/patches/patchClassComponent.js ================================================ import {defaults} from 'lodash'; import wdyrStore from '../wdyrStore'; import {checkIfInsideAStrictModeTree} from '../utils'; import getUpdateInfo from '../getUpdateInfo'; export default function patchClassComponent(ClassComponent, {displayName, defaultProps}) { class WDYRPatchedClassComponent extends ClassComponent { constructor(props, context) { super(props, context); this._WDYR = { renderNumber: 0, }; const origRender = super.render || this.render; // this probably means that render is an arrow function or this.render.bind(this) was called on the original class const renderIsABindedFunction = origRender !== ClassComponent.prototype.render; if (renderIsABindedFunction) { this.render = () => { WDYRPatchedClassComponent.prototype.render.apply(this); return origRender(); }; } } render() { this._WDYR.renderNumber++; if (!('isStrictMode' in this._WDYR)) { this._WDYR.isStrictMode = checkIfInsideAStrictModeTree(this); } // in strict mode- ignore every other render if (!(this._WDYR.isStrictMode && this._WDYR.renderNumber % 2 === 1)) { if (this._WDYR.prevProps) { const updateInfo = getUpdateInfo({ Component: ClassComponent, displayName, prevOwner: this._WDYR.prevOwner, prevProps: this._WDYR.prevProps, prevState: this._WDYR.prevState, nextOwner: wdyrStore.ownerBeforeElementCreation, nextProps: this.props, nextState: this.state, }); wdyrStore.options.notifier(updateInfo); } this._WDYR.prevOwner = wdyrStore.ownerBeforeElementCreation; this._WDYR.prevProps = this.props; this._WDYR.prevState = this.state; } return super.render ? super.render() : null; } } try { WDYRPatchedClassComponent.displayName = displayName; } catch (_e) { // not crucial if displayName couldn't be set } WDYRPatchedClassComponent.defaultProps = defaultProps; defaults(WDYRPatchedClassComponent, ClassComponent); return WDYRPatchedClassComponent; } ================================================ FILE: src/patches/patchForwardRefComponent.js ================================================ import {defaults} from 'lodash'; import wdyrStore from '../wdyrStore'; import getDisplayName from '../getDisplayName'; import {isMemoComponent} from '../utils'; import patchFunctionalOrStrComponent from './patchFunctionalOrStrComponent'; export default function patchForwardRefComponent(ForwardRefComponent, {displayName, defaultProps}) { const {render: InnerForwardRefComponent} = ForwardRefComponent; const isInnerComponentMemoized = isMemoComponent(InnerForwardRefComponent); const WrappedFunctionalComponent = isInnerComponentMemoized ? InnerForwardRefComponent.type : InnerForwardRefComponent; const WDYRWrappedByReactForwardRefFunctionalComponent = ( patchFunctionalOrStrComponent(WrappedFunctionalComponent, {isPure: isInnerComponentMemoized, displayName}) ); WDYRWrappedByReactForwardRefFunctionalComponent.displayName = getDisplayName(WrappedFunctionalComponent); WDYRWrappedByReactForwardRefFunctionalComponent.ComponentForHooksTracking = WrappedFunctionalComponent; defaults(WDYRWrappedByReactForwardRefFunctionalComponent, WrappedFunctionalComponent); const WDYRForwardRefFunctionalComponent = wdyrStore.React.forwardRef( isInnerComponentMemoized ? wdyrStore.React.memo(WDYRWrappedByReactForwardRefFunctionalComponent, InnerForwardRefComponent.compare) : WDYRWrappedByReactForwardRefFunctionalComponent ); try { WDYRForwardRefFunctionalComponent.displayName = displayName; } catch (_e) { // not crucial if displayName couldn't be set } WDYRForwardRefFunctionalComponent.defaultProps = defaultProps; defaults(WDYRForwardRefFunctionalComponent, ForwardRefComponent); return WDYRForwardRefFunctionalComponent; } ================================================ FILE: src/patches/patchFunctionalOrStrComponent.js ================================================ import {defaults} from 'lodash'; import wdyrStore from '../wdyrStore'; import getUpdateInfo from '../getUpdateInfo'; const getFunctionalComponentFromStringComponent = (componentTypeStr) => props => ( wdyrStore.React.createElement(componentTypeStr, props) ); export default function patchFunctionalOrStrComponent(FunctionalOrStringComponent, {isPure, displayName, defaultProps}) { const FunctionalComponent = typeof(FunctionalOrStringComponent) === 'string' ? getFunctionalComponentFromStringComponent(FunctionalOrStringComponent) : FunctionalOrStringComponent; function WDYRFunctionalComponent(nextProps, refMaybe, ...args) { const prevPropsRef = wdyrStore.React.useRef(); const prevProps = prevPropsRef.current; prevPropsRef.current = nextProps; const prevOwnerRef = wdyrStore.React.useRef(); const prevOwner = prevOwnerRef.current; const nextOwner = wdyrStore.ownerBeforeElementCreation; prevOwnerRef.current = nextOwner; if (prevProps) { const updateInfo = getUpdateInfo({ Component: FunctionalComponent, displayName, prevOwner, nextOwner, prevProps, nextProps, }); const notifiedByHooks = ( !updateInfo.reason.propsDifferences || ( (isPure && updateInfo.reason.propsDifferences.length === 0) ) ); if (!notifiedByHooks) { wdyrStore.options.notifier(updateInfo); } } return FunctionalComponent(nextProps, refMaybe, ...args); } try { WDYRFunctionalComponent.displayName = displayName; } catch (_e) { // not crucial if displayName couldn't be set } WDYRFunctionalComponent.defaultProps = defaultProps; WDYRFunctionalComponent.ComponentForHooksTracking = FunctionalComponent; defaults(WDYRFunctionalComponent, FunctionalComponent); return WDYRFunctionalComponent; } ================================================ FILE: src/patches/patchMemoComponent.js ================================================ import {defaults} from 'lodash'; import wdyrStore from '../wdyrStore'; import getDisplayName from '../getDisplayName'; import {isForwardRefComponent, isMemoComponent, isReactClassComponent} from '../utils'; import patchClassComponent from './patchClassComponent'; import patchFunctionalOrStrComponent from './patchFunctionalOrStrComponent'; export default function patchMemoComponent(MemoComponent, {displayName, defaultProps}) { const {type: InnerMemoComponent} = MemoComponent; const isInnerMemoComponentAClassComponent = isReactClassComponent(InnerMemoComponent); const isInnerMemoComponentForwardRefs = isForwardRefComponent(InnerMemoComponent); const isInnerMemoComponentAnotherMemoComponent = isMemoComponent(InnerMemoComponent); const WrappedFunctionalComponent = isInnerMemoComponentForwardRefs ? InnerMemoComponent.render : InnerMemoComponent; const PatchedInnerComponent = isInnerMemoComponentAClassComponent ? patchClassComponent(WrappedFunctionalComponent, {displayName, defaultProps}) : (isInnerMemoComponentAnotherMemoComponent ? patchMemoComponent(WrappedFunctionalComponent, {displayName, defaultProps}) : patchFunctionalOrStrComponent(WrappedFunctionalComponent, {displayName, isPure: true}) ); try { PatchedInnerComponent.displayName = getDisplayName(WrappedFunctionalComponent); } catch (_e) { // not crucial if displayName couldn't be set } PatchedInnerComponent.ComponentForHooksTracking = MemoComponent; defaults(PatchedInnerComponent, WrappedFunctionalComponent); const WDYRMemoizedFunctionalComponent = wdyrStore.React.memo( isInnerMemoComponentForwardRefs ? wdyrStore.React.forwardRef(PatchedInnerComponent) : PatchedInnerComponent, MemoComponent.compare ); try { WDYRMemoizedFunctionalComponent.displayName = displayName; } catch (_e) { // not crucial if displayName couldn't be set } WDYRMemoizedFunctionalComponent.defaultProps = defaultProps; defaults(WDYRMemoizedFunctionalComponent, MemoComponent); return WDYRMemoizedFunctionalComponent; } ================================================ FILE: src/printDiff.js ================================================ import {sortBy, groupBy} from 'lodash'; import calculateDeepEqualDiffs from './calculateDeepEqualDiffs'; import {diffTypesDescriptions} from './consts'; export default function printDiff(value1, value2, {pathString, consoleLog}) { const diffs = calculateDeepEqualDiffs(value1, value2, pathString, {detailed: true}); const keysLength = Math.max(...diffs.map(diff => diff.pathString.length)) + 2; Object.entries(groupBy(sortBy(diffs, 'pathString'), 'diffType')) .forEach(([diffType, diffs]) => { consoleLog(`%c${diffTypesDescriptions[diffType]}:`, 'text-decoration: underline; color: blue;'); diffs.forEach(diff => { consoleLog(`${diff.pathString}:`.padEnd(keysLength, ' '), diff.prevValue); }); }); } ================================================ FILE: src/shouldTrack.js ================================================ import wdyrStore from './wdyrStore'; import {isMemoComponent} from './utils'; import getDisplayName from './getDisplayName'; function shouldInclude(displayName) { return ( wdyrStore.options.include && wdyrStore.options.include.length > 0 && wdyrStore.options.include.some(regex => regex.test(displayName)) ); } function shouldExclude(displayName) { return ( wdyrStore.options.exclude && wdyrStore.options.exclude.length > 0 && wdyrStore.options.exclude.some(regex => regex.test(displayName)) ); } export default function shouldTrack(Component, {isHookChange}) { const displayName = getDisplayName(Component); if (shouldExclude(displayName)) { return false; } if (Component.whyDidYouRender === false) { return false; } if (isHookChange && ( Component.whyDidYouRender && Component.whyDidYouRender.trackHooks === false )) { return false; } return !!( Component.whyDidYouRender || ( wdyrStore.options.trackAllPureComponents && ( (Component && Component.prototype instanceof wdyrStore.React.PureComponent) || isMemoComponent(Component) ) ) || shouldInclude(displayName) ); } ================================================ FILE: src/utils.js ================================================ // copied from https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactTypeOfMode.js import {REACT_FORWARD_REF_TYPE, REACT_MEMO_TYPE, REACT_STRICT_MODE} from './consts'; // based on "findStrictRoot" from https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactStrictModeWarnings.js // notice: this is only used for class components. functional components doesn't render twice inside strict mode export function checkIfInsideAStrictModeTree(reactComponentInstance) { let reactInternalFiber = reactComponentInstance && ( reactComponentInstance._reactInternalFiber || reactComponentInstance._reactInternals ); while (reactInternalFiber) { if (reactInternalFiber.mode & REACT_STRICT_MODE) { return true; } reactInternalFiber = reactInternalFiber.return; } return false; } export function isReactClassComponent(Component) { return Component.prototype && !!Component.prototype.isReactComponent; } export function isMemoComponent(Component) { return Component.$$typeof === REACT_MEMO_TYPE; } export function isForwardRefComponent(Component) { return Component.$$typeof === REACT_FORWARD_REF_TYPE; } ================================================ FILE: src/wdyrStore.js ================================================ const wdyrStore = { /* The React object we patch */ React: undefined, /* Processed user options for WDYR */ options: undefined, /* The original React.createElement function */ origCreateElement: undefined, /* The original React.createFactory function */ origCreateFactory: undefined, /* The original React.cloneElement function */ origCloneElement: undefined, /* A weak map of all React elements to their WDYR patched react elements */ componentsMap: new WeakMap(), /* A weak map of props to the owner element that passed them */ ownerDataMap: new WeakMap(), /* An array of infos for hooks tracked during current render */ hooksInfoForCurrentRender: new WeakMap(), /* Owner before element creation started */ ownerBeforeElementCreation: null, }; export default wdyrStore; ================================================ FILE: src/whyDidYouRender.js ================================================ import {get, isFunction} from 'lodash'; import wdyrStore from './wdyrStore'; import normalizeOptions from './normalizeOptions'; import getDisplayName from './getDisplayName'; import getDefaultProps from './getDefaultProps'; import getUpdateInfo from './getUpdateInfo'; import shouldTrack from './shouldTrack'; import patchClassComponent from './patches/patchClassComponent'; import patchFunctionalOrStrComponent from './patches/patchFunctionalOrStrComponent'; import patchMemoComponent from './patches/patchMemoComponent'; import patchForwardRefComponent from './patches/patchForwardRefComponent'; import { isForwardRefComponent, isMemoComponent, isReactClassComponent, } from './utils'; import {dependenciesMap} from './calculateDeepEqualDiffs'; import {getCurrentOwner} from './helpers'; export {wdyrStore, getCurrentOwner}; const initialHookValue = Symbol('initial-hook-value'); function trackHookChanges(hookName, {path: pathToGetTrackedHookResult}, rawHookResult) { const nextResult = pathToGetTrackedHookResult ? get(rawHookResult, pathToGetTrackedHookResult) : rawHookResult; const prevResultRef = wdyrStore.React.useRef(initialHookValue); const prevResult = prevResultRef.current; prevResultRef.current = nextResult; const ownerInstance = getCurrentOwner(); if (!ownerInstance) { return rawHookResult; } if (!wdyrStore.hooksInfoForCurrentRender.has(ownerInstance)) { wdyrStore.hooksInfoForCurrentRender.set(ownerInstance, []); } const hooksInfoForCurrentRender = wdyrStore.hooksInfoForCurrentRender.get(ownerInstance); hooksInfoForCurrentRender.push({hookName, result: nextResult}); const Component = ownerInstance.type.ComponentForHooksTracking || ownerInstance.type; const displayName = getDisplayName(Component); const isShouldTrack = shouldTrack(Component, {isHookChange: true}); if (isShouldTrack && prevResult !== initialHookValue) { const updateInfo = getUpdateInfo({ Component: Component, displayName, hookName, prevHookResult: prevResult, nextHookResult: nextResult, }); if (updateInfo.reason.hookDifferences) { wdyrStore.options.notifier(updateInfo); } } return rawHookResult; } function createPatchedComponent(Component, {displayName, defaultProps}) { if (isMemoComponent(Component)) { return patchMemoComponent(Component, {displayName, defaultProps}); } if (isForwardRefComponent(Component)) { return patchForwardRefComponent(Component, {displayName, defaultProps}); } if (isReactClassComponent(Component)) { return patchClassComponent(Component, {displayName, defaultProps}); } return patchFunctionalOrStrComponent(Component, {displayName, defaultProps, isPure: false}); } function getPatchedComponent(Component, {displayName, defaultProps}) { if (wdyrStore.componentsMap.has(Component)) { return wdyrStore.componentsMap.get(Component); } const WDYRPatchedComponent = createPatchedComponent(Component, {displayName, defaultProps}); wdyrStore.componentsMap.set(Component, WDYRPatchedComponent); return WDYRPatchedComponent; } function getIsSupportedComponentType(Comp) { if (!Comp) { return false; } if (isMemoComponent(Comp)) { return getIsSupportedComponentType(Comp.type); } if (isForwardRefComponent(Comp)) { return getIsSupportedComponentType(Comp.render); } if (typeof Comp === 'function') { return true; } } export const hooksConfig = { useState: {path: '0'}, useReducer: {path: '0'}, useContext: undefined, useSyncExternalStore: undefined, useMemo: {dependenciesPath: '1', dontReport: true}, useCallback: {dependenciesPath: '1', dontReport: true}, }; export function storeOwnerData(element) { const owner = getCurrentOwner(); if (owner) { const Component = owner.type.ComponentForHooksTracking || owner.type; const displayName = getDisplayName(Component); let additionalOwnerData = {}; if (wdyrStore.options.getAdditionalOwnerData) { additionalOwnerData = wdyrStore.options.getAdditionalOwnerData(element); } wdyrStore.ownerDataMap.set(owner, { Component, displayName, props: owner.pendingProps, state: owner.stateNode ? owner.stateNode.state : null, hooksInfo: wdyrStore.hooksInfoForCurrentRender.get(owner) || [], additionalOwnerData, }); wdyrStore.hooksInfoForCurrentRender.delete(owner); } } function trackHooksIfNeeded() { const hooksSupported = !!wdyrStore.React.useState; if (wdyrStore.options.trackHooks && hooksSupported) { const nativeHooks = Object.entries(hooksConfig).map(([hookName, hookTrackingConfig]) => { return [wdyrStore.React, hookName, hookTrackingConfig]; }); const hooksToTrack = [ ...nativeHooks, ...wdyrStore.options.trackExtraHooks, ]; hooksToTrack.forEach(([hookParent, hookName, hookTrackingConfig = {}]) => { const originalHook = hookParent[hookName]; const newHook = function useWhyDidYouRenderReWrittenHook(...args) { const hookResult = originalHook.call(this, ...args); const {dependenciesPath, dontReport} = hookTrackingConfig; const shouldTrackHookChanges = !dontReport; if (dependenciesPath && isFunction(hookResult)) { dependenciesMap.set(hookResult, {hookName, deps: get(args, dependenciesPath)}); } if (shouldTrackHookChanges) { trackHookChanges(hookName, hookTrackingConfig, hookResult); } return hookResult; }; Object.defineProperty(newHook, 'name', { value: hookName + 'WDYR', writable: false }); Object.assign(newHook, {originalHook}); hookParent[hookName] = newHook; }); } } export function getWDYRType(origType) { const isShouldTrack = ( getIsSupportedComponentType(origType) && shouldTrack(origType, {isHookChange: false}) ); if (!isShouldTrack) { return null; } const displayName = ( origType && origType.whyDidYouRender && origType.whyDidYouRender.customName || getDisplayName(origType) ); const defaultProps = getDefaultProps(origType); const WDYRPatchedComponent = getPatchedComponent(origType, {displayName, defaultProps}); return WDYRPatchedComponent; } export default function whyDidYouRender(React, userOptions) { if (React.__IS_WDYR__) { return; } React.__IS_WDYR__ = true; Object.assign(wdyrStore, { React, options: normalizeOptions(userOptions), origCreateElement: React.createElement, origCreateFactory: React.createFactory, origCloneElement: React.cloneElement, componentsMap: new WeakMap(), }); React.createElement = function(origType, ...rest) { const WDYRType = getWDYRType(origType); if (WDYRType) { try { wdyrStore.ownerBeforeElementCreation = getCurrentOwner(); const element = wdyrStore.origCreateElement.apply(React, [WDYRType, ...rest]); if (wdyrStore.options.logOwnerReasons) { storeOwnerData(element); } return element; } catch (e) { wdyrStore.options.consoleLog('whyDidYouRender error in createElement. Please file a bug at https://github.com/welldone-software/why-did-you-render/issues.', { errorInfo: { error: e, componentNameOrComponent: origType, rest, options: wdyrStore.options, }, }); } } return wdyrStore.origCreateElement.apply(React, [origType, ...rest]); }; Object.assign(React.createElement, wdyrStore.origCreateElement); React.createFactory = type => { const factory = React.createElement.bind(null, type); factory.type = type; return factory; }; Object.assign(React.createFactory, wdyrStore.origCreateFactory); React.cloneElement = (...args) => { wdyrStore.ownerBeforeElementCreation = getCurrentOwner(); const element = wdyrStore.origCloneElement.apply(React, args); if (wdyrStore.options.logOwnerReasons) { storeOwnerData(element); } return element; }; Object.assign(React.cloneElement, wdyrStore.origCloneElement); trackHooksIfNeeded(); React.__REVERT_WHY_DID_YOU_RENDER__ = () => { Object.assign(React, { createElement: wdyrStore.origCreateElement, createFactory: wdyrStore.origCreateFactory, cloneElement: wdyrStore.origCloneElement, }); wdyrStore.componentsMap = null; const hooksToRevert = [ ...Object.keys(hooksConfig).map(hookName => [React, hookName]), ...wdyrStore.options.trackExtraHooks, ]; hooksToRevert.forEach(([hookParent, hookName]) => { if (hookParent[hookName].originalHook) { hookParent[hookName] = hookParent[hookName].originalHook; } }); delete React.__REVERT_WHY_DID_YOU_RENDER__; delete React.__IS_WDYR__; }; return React; } ================================================ FILE: tests/.eslintrc ================================================ { "extends": [ "plugin:jest/recommended", "../.eslintrc" ], "rules": { "jest/expect-expect": "off", "jest/valid-title": "off" } } ================================================ FILE: tests/babel.config.cjs ================================================ module.exports = require('../babel.config'); ================================================ FILE: tests/calculateDeepEqualDiffs.test.js ================================================ import React from 'react'; import calculateDeepEqualDiffs from '~/calculateDeepEqualDiffs'; import {diffTypes} from '~/consts'; test('same', () => { const prevValue = {a: 'b'}; const nextValue = prevValue; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([]); }); test('not deep equal', () => { const prevValue = {a: 'b'}; const nextValue = {a: 'c'}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '.a', prevValue: 'b', nextValue: 'c', diffType: diffTypes.different, }, { pathString: '', prevValue, nextValue, diffType: diffTypes.different, }, ]); }); test('simple deep', () => { const prevValue = {a: 'b'}; const nextValue = {a: 'b'}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('nested object deep equals', () => { const prevValue = {a: {b: 'c'}}; const nextValue = {a: {b: 'c'}}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('nested array deep equals', () => { const prevValue = {a: {b: ['c']}}; const nextValue = {a: {b: ['c']}}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('date', () => { const now = new Date(); const now2 = new Date(now); const diffs = calculateDeepEqualDiffs(now, now2); expect(diffs).toEqual([ { pathString: '', prevValue: now, nextValue: now2, diffType: diffTypes.date, }, ]); }); test('nested date', () => { const now = new Date(); const now2 = new Date(now); const prevValue = {a: {b: [now]}}; const nextValue = {a: {b: [now2]}}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('regular expression', () => { const regEx = /c/i; const regEx2 = /c/i; const diffs = calculateDeepEqualDiffs(regEx, regEx2); expect(diffs).toEqual([ { pathString: '', prevValue: regEx, nextValue: regEx2, diffType: diffTypes.regex, }, ]); }); test('nested regular expression', () => { const regEx = /c/i; const regEx2 = /c/i; const prevValue = {a: {b: [regEx]}}; const nextValue = {a: {b: [regEx2]}}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('dom elements', () => { const element = document.createElement('div'); const element2 = document.createElement('div'); const prevValue = {a: element}; const nextValue = {a: element2}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '.a', prevValue: prevValue.a, nextValue: nextValue.a, diffType: diffTypes.different, }, { pathString: '', prevValue, nextValue, diffType: diffTypes.different, }, ]); }); test('equal react elements', () => { const tooltip =
    hi!
    ; const prevValue = {a: tooltip}; const nextValue = {a: tooltip}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('simple react elements', () => { const tooltip =
    hi!
    ; const tooltip2 =
    hi!
    ; const diffs = calculateDeepEqualDiffs(tooltip, tooltip2); expect(diffs).toEqual([ { pathString: '', prevValue: tooltip, nextValue: tooltip2, diffType: diffTypes.reactElement, }, ]); }); test('nested react elements', () => { const tooltip =
    hi!
    ; const tooltip2 =
    hi!
    ; const prevValue = {a: tooltip}; const nextValue = {a: tooltip2}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('nested different react elements', () => { const tooltip =
    hi!
    ; const tooltip2 =
    hi 2 !
    ; const prevValue = {a: tooltip}; const nextValue = {a: tooltip2}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '.a', prevValue: tooltip, nextValue: tooltip2, diffType: diffTypes.different, }, { pathString: '', prevValue, nextValue, diffType: diffTypes.different, }, ]); }); test('nested different react elements with several children', () => { const prevValue = ; const nextValue = ; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.different, }, ]); }); test('nested different react elements with several children with keys', () => { const prevValue = ; const nextValue = ; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.different, }, ]); }); test('react class component instance', () => { class MyComponent extends React.Component { render() { return
    hi!
    ; } } const tooltip = ; const tooltip2 = ; const prevValue = {a: tooltip}; const nextValue = {a: tooltip2}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('react class pure component instance', () => { class MyComponent extends React.PureComponent { render() { return
    hi!
    ; } } const tooltip = ; const tooltip2 = ; const prevValue = {a: tooltip}; const nextValue = {a: tooltip2}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('react functional component instance', () => { const MyFunctionalComponent = () => (
    hi!
    ); const tooltip = ; const tooltip2 = ; const prevValue = {a: tooltip}; const nextValue = {a: tooltip2}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('react memoized functional component instance', () => { const MyFunctionalComponent = React.memo(() => (
    hi!
    )); const tooltip = ; const tooltip2 = ; const prevValue = {a: tooltip}; const nextValue = {a: tooltip2}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('functions', () => { const fn = function something() {}; const fn2 = function something() {}; const prevValue = {fn}; const nextValue = {fn: fn2}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('inline functions', () => { const prevValue = {a: {fn: () => {}}}; const nextValue = {a: {fn: () => {}}}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('sets', () => { const prevValue = { a: new Set(['a']), b: new Set(['a', 1]), c: new Set(['a', 1]), }; const nextValue = { a: new Set(['a']), b: new Set(['a', 2]), c: new Set(['a', 1, 'c']), }; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '.c', prevValue: prevValue.c, nextValue: nextValue.c, diffType: diffTypes.different, }, { pathString: '.b', prevValue: prevValue.b, nextValue: nextValue.b, diffType: diffTypes.different, }, { pathString: '.a', prevValue: prevValue.a, nextValue: nextValue.a, diffType: diffTypes.deepEquals, }, { pathString: '', prevValue: prevValue, nextValue: nextValue, diffType: diffTypes.different, }, ]); }); test('mix', () => { const prevValue = {a: {fn: () => {}}, b: [{tooltip:
    hi
    }]}; const nextValue = {a: {fn: () => {}}, b: [{tooltip:
    hi
    }]}; const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); describe('calculateDeepEqualDiffs - Errors', () => { test('Equal Native Errors', () => { const prevValue = new Error('message'); const nextValue = new Error('message'); const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('Different Native Errors', () => { const prevValue = new Error('message'); const nextValue = new Error('Second message'); const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '.message', prevValue: 'message', nextValue: 'Second message', diffType: diffTypes.different, }, { pathString: '', prevValue, nextValue, diffType: diffTypes.different, }, ]); }); test('Equal Custom Errors', () => { class CustomError extends Error { constructor(message, code) { super(message); this.name = 'ValidationError'; this.code = code; } } const prevValue = new CustomError('message', 1001); const nextValue = new CustomError('message', 1001); const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('Different Custom Errors', () => { class CustomError extends Error { constructor(message, code) { super(message); this.name = 'ValidationError'; this.code = code; } } const prevValue = new CustomError('message', 1001); const nextValue = new CustomError('message', 1002); const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '.code', prevValue: 1001, nextValue: 1002, diffType: diffTypes.different, }, { pathString: '', prevValue, nextValue, diffType: diffTypes.different, }, ]); }); }); test('Equal class instances', () => { class Person { constructor(name) { this.name = name; } } const prevValue = new Person('Jon Snow'); const nextValue = new Person('Jon Snow'); const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '', prevValue, nextValue, diffType: diffTypes.deepEquals, }, ]); }); test('Different class instances', () => { class Person { constructor(name) { this.name = name; } } const prevValue = new Person('Jon Snow'); const nextValue = new Person('Aria Stark'); const diffs = calculateDeepEqualDiffs(prevValue, nextValue); expect(diffs).toEqual([ { pathString: '.name', prevValue: 'Jon Snow', nextValue: 'Aria Stark', diffType: diffTypes.different, }, { pathString: '', prevValue: { name: 'Jon Snow', }, nextValue: { name: 'Aria Stark', }, diffType: diffTypes.different, }, ]); }); ================================================ FILE: tests/defaultNotifier.test.js ================================================ import React from 'react'; import defaultNotifier from '~/defaultNotifier'; import getUpdateInfo from '~/getUpdateInfo'; import whyDidYouRender from '~'; class TestComponent extends React.Component { static whyDidYouRender = true; render() { return
    hi!
    ; } } const testInputAndExpects = { default: { description: 'Group by component (default options)', userOptions: undefined, expects: { logsCount: { title: 0, emptyValues: 1, changedObjects: 2, changedObjectValues: 3, changedObjectValuesDeepEquals: 4, }, groupLogsCount: { title: 1, emptyValues: 0, changedObjects: 0, changedObjectValues: 1, changedObjectValuesDeepEquals: 1, }, groupCollapsedLogsCount: { title: 0, emptyValues: 0, changedObjects: 0, changedObjectValues: 0, changedObjectValuesDeepEquals: 0, }, }, }, onlyLogs: { description: 'Only logs', userOptions: {onlyLogs: true}, expects: { logsCount: { title: 1, emptyValues: 1, changedObjects: 2, changedObjectValues: 4, changedObjectValuesDeepEquals: 5, }, groupLogsCount: { title: 0, emptyValues: 0, changedObjects: 0, changedObjectValues: 0, changedObjectValuesDeepEquals: 0, }, groupCollapsedLogsCount: { title: 0, emptyValues: 0, changedObjects: 0, changedObjectValues: 0, changedObjectValuesDeepEquals: 0, }, }, }, collapseGroups: { description: 'Group by component with collapse', userOptions: {collapseGroups: true}, expects: { logsCount: { title: 0, emptyValues: 1, changedObjects: 2, changedObjectValues: 3, changedObjectValuesDeepEquals: 4, }, groupLogsCount: { title: 0, emptyValues: 0, changedObjects: 0, changedObjectValues: 0, changedObjectValuesDeepEquals: 0, }, groupCollapsedLogsCount: { title: 1, emptyValues: 0, changedObjects: 0, changedObjectValues: 1, changedObjectValuesDeepEquals: 1, }, }, }, }; function calculateNumberOfExpectedLogs(expectedLogTypes, expectedCounts) { return expectedLogTypes.reduce((sum, type) => sum + expectedCounts[type], 0); } function expectLogTypes(expectedLogTypes, expects) { const consoleOutputs = flushConsoleOutput(); expect(consoleOutputs.filter(o => o.level === 'log')) .toHaveLength(calculateNumberOfExpectedLogs(expectedLogTypes, expects.logsCount)); expect(consoleOutputs.filter(o => o.level === 'group')) .toHaveLength(calculateNumberOfExpectedLogs(expectedLogTypes, expects.groupLogsCount)); expect(consoleOutputs.filter(o => o.level === 'groupCollapsed')) .toHaveLength(calculateNumberOfExpectedLogs(expectedLogTypes, expects.groupCollapsedLogsCount)); } describe('For no differences', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: null, prevState: null, nextProps: null, nextState: null, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'emptyValues'], expects); }); }); }); describe('For different props eq by ref', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: {a: 'aa'}, prevState: null, nextProps: {a: 'aa'}, nextState: null, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjects'], expects); }); }); }); describe('For equal state eq by ref', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: null, prevState: {a: 'aa'}, nextProps: null, nextState: {a: 'aa'}, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjects'], expects); }); }); }); describe('For different state and props', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: {a: 'aa'}, prevState: {a: 'aa'}, nextProps: {a: 'aa'}, nextState: {a: 'aa'}, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjects', 'changedObjects'], expects); }); }); }); describe('For different hook', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevHookResult: {a: 'aa'}, nextHookResult: {a: 'aa'}, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjectValuesDeepEquals'], expects); }); }); }); describe('For different deep equal props', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: {a: {b: 'b'}}, prevState: null, nextProps: {a: {b: 'b'}}, nextState: null, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjectValuesDeepEquals'], expects); }); }); }); describe('For different deep equal state', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: null, prevState: {a: {b: 'b'}}, nextProps: null, nextState: {a: {b: 'b'}}, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjectValuesDeepEquals'], expects); }); }); }); describe('For different deep equal state and props', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: {a: {b: 'b'}}, prevState: {a: {b: 'b'}}, nextProps: {a: {b: 'b'}}, nextState: {a: {b: 'b'}}, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjectValuesDeepEquals', 'changedObjectValuesDeepEquals'], expects); }); }); }); describe('For different functions by the same name', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: {fn: function something() {}}, prevState: null, nextProps: {fn: function something() {}}, nextState: null, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjectValues'], expects); }); }); }); describe('Mix of changes', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); Object.values(testInputAndExpects).forEach(({description, userOptions, expects}) => { test(description, () => { whyDidYouRender(React, userOptions); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: {fn: function something() {}}, prevState: {a: {b: 'b'}}, nextProps: {fn: function something() {}}, nextState: {a: {b: 'b'}}, }); defaultNotifier(updateInfo); expectLogTypes(['title', 'changedObjectValues', 'changedObjectValuesDeepEquals'], expects); }); }); }); describe('logOnDifferentProps option', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('For different props', () => { whyDidYouRender(React, {onlyLogs: true}); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: {a: 'aaaa'}, prevState: null, nextProps: {a: 'bbbb'}, nextState: null, }); defaultNotifier(updateInfo); const consoleOutputs = flushConsoleOutput(); expect(consoleOutputs).toHaveLength(0); }); test('For different state', () => { whyDidYouRender(React, {onlyLogs: true}); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: null, prevState: {a: 'aaaa'}, nextProps: null, nextState: {a: 'bbbb'}, }); defaultNotifier(updateInfo); const consoleOutputs = flushConsoleOutput(); expect(consoleOutputs).toHaveLength(0); }); test('For different props with logOnDifferentValues', () => { whyDidYouRender(React, {logOnDifferentValues: true, onlyLogs: true}); const updateInfo = getUpdateInfo({ Component: TestComponent, prevProps: {a: 'aaaa'}, prevState: null, nextProps: {a: 'bbbb'}, nextState: null, }); defaultNotifier(updateInfo); const consoleOutputs = flushConsoleOutput(); expect(consoleOutputs).toHaveLength( calculateNumberOfExpectedLogs( ['title', 'changedObjectValues'], testInputAndExpects.onlyLogs.expects.logsCount ) ); }); test('For different props with logOnDifferentValues for a specific component', () => { whyDidYouRender(React, {onlyLogs: true}); class OwnTestComponent extends React.Component { static whyDidYouRender = {logOnDifferentValues: true}; render() { return
    hi!
    ; } } const updateInfo = getUpdateInfo({ Component: OwnTestComponent, prevProps: {a: 'aaaa'}, prevState: null, nextProps: {a: 'bbbb'}, nextState: null, }); defaultNotifier(updateInfo); const consoleOutputs = flushConsoleOutput(); expect(consoleOutputs).toHaveLength( calculateNumberOfExpectedLogs( ['title', 'changedObjectValues'], testInputAndExpects.onlyLogs.expects.logsCount ) ); }); }); ================================================ FILE: tests/findObjectsDifferences.test.js ================================================ import findObjectsDifferences from '~/findObjectsDifferences'; import {diffTypes} from '~/consts'; describe('findObjectsDifferences shallow', () => { test('for empty values', () => { const prev = null; const next = null; const diffs = findObjectsDifferences(prev, next); expect(diffs).toEqual(false); }); test('For no differences', () => { const prev = {prop: 'value'}; const next = prev; const diffs = findObjectsDifferences(prev, next); expect(diffs).toEqual(false); }); test('For prev empty value', () => { const prev = null; const next = {prop: 'value'}; const diffs = findObjectsDifferences(prev, next); expect(diffs).toEqual([ { pathString: 'prop', diffType: diffTypes.different, prevValue: undefined, nextValue: 'value', }, ]); }); test('For next empty value', () => { const prev = {prop: 'value'}; const next = null; const diffs = findObjectsDifferences(prev, next); expect(diffs).toEqual([ { pathString: 'prop', diffType: diffTypes.different, prevValue: 'value', nextValue: undefined, }, ]); }); test('For objects different by reference but equal by value', () => { const prop2 = {a: 'a'}; const prev = {prop: 'value', prop2}; const next = {prop: 'value', prop2}; const diffs = findObjectsDifferences(prev, next); expect(diffs).toEqual([]); }); test('For props inside the object different by reference but equal by value', () => { const prev = {prop: {a: 'a'}}; const next = {prop: {a: 'a'}}; const diffs = findObjectsDifferences(prev, next); expect(diffs).toEqual([ { pathString: 'prop', diffType: diffTypes.deepEquals, prevValue: prev.prop, nextValue: next.prop, }, ]); }); test('For functions inside the object with the same name', () => { const prev = {fn: function something() {}}; const next = {fn: function something() {}}; const diffs = findObjectsDifferences(prev, next); expect(diffs).toEqual([ { pathString: 'fn', diffType: diffTypes.function, prevValue: prev.fn, nextValue: next.fn, }, ]); }); test('Mix of differences inside the objects', () => { const prev = {prop: 'value', prop2: {a: 'a'}, prop3: 'AA', fn: function something() {}}; const next = {prop: 'value', prop2: {a: 'a'}, prop3: 'ZZ', fn: function something() {}}; const diffs = findObjectsDifferences(prev, next); expect(diffs).toEqual([ { pathString: 'prop2', diffType: diffTypes.deepEquals, prevValue: prev.prop2, nextValue: next.prop2, }, { pathString: 'prop3', diffType: diffTypes.different, prevValue: prev.prop3, nextValue: next.prop3, }, { pathString: 'fn', diffType: diffTypes.function, prevValue: prev.fn, nextValue: next.fn, }, ]); }); }); describe('findObjectsDifferences not shallow', () => { test('for empty values', () => { const prev = null; const next = null; const diffs = findObjectsDifferences(prev, next, {shallow: false}); expect(diffs).toEqual(false); }); test('For no differences', () => { const prev = {prop: 'value'}; const next = prev; const diffs = findObjectsDifferences(prev, next, {shallow: false}); expect(diffs).toEqual(false); }); test('For prev empty value', () => { const prev = null; const next = {prop: 'value'}; const diffs = findObjectsDifferences(prev, next, {shallow: false}); expect(diffs).toEqual([ { pathString: '', diffType: diffTypes.different, prevValue: null, nextValue: {prop: 'value'}, }, ]); }); test('For next empty value', () => { const prev = {prop: 'value'}; const next = null; const diffs = findObjectsDifferences(prev, next, {shallow: false}); expect(diffs).toEqual([ { pathString: '', diffType: diffTypes.different, prevValue: {prop: 'value'}, nextValue: null, }, ]); }); test('For objects different by reference but equal by value', () => { const prop2 = {a: 'a'}; const prev = {prop: 'value', prop2}; const next = {prop: 'value', prop2}; const diffs = findObjectsDifferences(prev, next, {shallow: false}); expect(diffs).toEqual([ { pathString: '', diffType: diffTypes.deepEquals, prevValue: {prop: 'value', prop2}, nextValue: {prop: 'value', prop2}, }, ]); }); test('For sets with same values', () => { const prev = new Set([1, 2, 3]); const next = new Set([1, 2, 3]); const diffs = findObjectsDifferences(prev, next, {shallow: false}); expect(diffs).toEqual([{ pathString: '', diffType: diffTypes.deepEquals, prevValue: prev, nextValue: next, }]); }); test('For sets with different values', () => { const prev = new Set([1, 2, 3]); const next = new Set([4, 5, 6]); const diffs = findObjectsDifferences(prev, next, {shallow: false}); expect(diffs).toEqual([ { pathString: '', diffType: diffTypes.different, prevValue: prev, nextValue: next, }, ]); }); test('For sets with different value length', () => { const prev = new Set([1, 2, 3]); const next = new Set([1, 2, 3, 4]); const diffs = findObjectsDifferences(prev, next, {shallow: false}); expect(diffs).toEqual([ { pathString: '', diffType: diffTypes.different, prevValue: prev, nextValue: next, }, ]); }); }); ================================================ FILE: tests/getDisplayName.test.js ================================================ import React from 'react'; import getDisplayName from '~/getDisplayName'; test('For a component', () => { class TestComponent extends React.Component { render() { return
    hi!
    ; } } const displayName = getDisplayName(TestComponent); expect(displayName).toBe('TestComponent'); }); test('For inline functions', () => { const InlineComponent = () => (
    hi!
    ); InlineComponent.displayName = 'InlineComponentCustomName'; const displayName = getDisplayName(InlineComponent); expect(displayName).toBe('InlineComponentCustomName'); }); test('For inline functions with no name', () => { const InlineComponent = () => (
    hi!
    ); const displayName = getDisplayName(InlineComponent); expect(displayName).toBe('InlineComponent'); }); ================================================ FILE: tests/getUpdateInfo.test.js ================================================ import React from 'react'; import {diffTypes} from '~/consts'; import getUpdateInfo from '~/getUpdateInfo'; import getDisplayName from '~/getDisplayName'; import whyDidYouRender from '~'; class TestComponent extends React.Component { render() { return
    hi!
    ; } } describe('getUpdateInfo', () => { beforeEach(() => { whyDidYouRender(React); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('Empty props and state', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {}, prevState: null, nextProps: {}, nextState: null, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, ownerDataMap: expect.any(WeakMap), displayName: 'TestComponent', reason: { propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }, }); }); test('Same props', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {a: 1}, prevState: null, nextProps: {a: 1}, nextState: null, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, ownerDataMap: expect.any(WeakMap), displayName: 'TestComponent', reason: { propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }, }); }); test('Same state', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {}, prevState: {a: 1}, nextProps: {}, nextState: {a: 1}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, ownerDataMap: expect.any(WeakMap), displayName: 'TestComponent', reason: { propsDifferences: [], stateDifferences: [], hookDifferences: false, ownerDifferences: false, }, }); }); test('Same props and state', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {b: 1}, prevState: {a: 1}, nextProps: {b: 1}, nextState: {a: 1}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, ownerDataMap: expect.any(WeakMap), displayName: 'TestComponent', reason: { propsDifferences: [], stateDifferences: [], hookDifferences: false, ownerDifferences: false, }, }); }); test('Props change', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {a: 1}, prevState: null, nextProps: {a: 2}, nextState: null, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [ { pathString: 'a', diffType: diffTypes.different, prevValue: input.prevProps.a, nextValue: input.nextProps.a, }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }, }); }); test('State change', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {}, prevState: {a: 1}, nextProps: {}, nextState: {a: 2}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [], stateDifferences: [ { pathString: 'a', diffType: diffTypes.different, prevValue: input.prevState.a, nextValue: input.nextState.a, }, ], hookDifferences: false, ownerDifferences: false, }, }); }); test('Props and state change', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {b: 1}, prevState: {a: 1}, nextProps: {b: 2}, nextState: {a: 2}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [ { pathString: 'b', diffType: diffTypes.different, prevValue: input.prevProps.b, nextValue: input.nextProps.b, }, ], stateDifferences: [ { pathString: 'a', diffType: diffTypes.different, prevValue: input.prevState.a, nextValue: input.nextState.a, }, ], hookDifferences: false, ownerDifferences: false, }, }); }); test('Props change by ref', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {a: {b: 'b'}}, prevState: null, nextProps: {a: {b: 'b'}}, nextState: null, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: input.prevProps.a, nextValue: input.nextProps.a, }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }, }); }); test('State changed by ref', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {}, prevState: {a: {b: 'b'}}, nextProps: {}, nextState: {a: {b: 'b'}}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [], stateDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: input.prevState.a, nextValue: input.nextState.a, }, ], hookDifferences: false, ownerDifferences: false, }, }); }); test('Props and state different by ref', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {b: {c: 'c'}}, prevState: {a: {d: 'd'}}, nextProps: {b: {c: 'c'}}, nextState: {a: {d: 'd'}}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [ { pathString: 'b', diffType: diffTypes.deepEquals, prevValue: input.prevProps.b, nextValue: input.nextProps.b, }, ], stateDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: input.prevState.a, nextValue: input.nextState.a, }, ], hookDifferences: false, ownerDifferences: false, }, }); }); test('Props change by function', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {a: () => {}}, prevState: null, nextProps: {a: () => {}}, nextState: null, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [ { pathString: 'a', diffType: diffTypes.function, prevValue: input.prevProps.a, nextValue: input.nextProps.a, }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }, }); }); test('State changed by function ref', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {}, prevState: {a: () => {}}, nextProps: {}, nextState: {a: () => {}}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [], stateDifferences: [ { pathString: 'a', diffType: diffTypes.function, prevValue: input.prevState.a, nextValue: input.nextState.a, }, ], hookDifferences: false, ownerDifferences: false, }, }); }); test('Props and state different by function', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {a: () => {}}, prevState: {b: () => {}}, nextProps: {a: () => {}}, nextState: {b: () => {}}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [ { pathString: 'a', diffType: diffTypes.function, prevValue: input.prevProps.a, nextValue: input.nextProps.a, }, ], stateDifferences: [ { pathString: 'b', diffType: diffTypes.function, prevValue: input.prevState.b, nextValue: input.nextState.b, }, ], hookDifferences: false, ownerDifferences: false, }, }); }); test('Mix of differences', () => { const input = { Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps: {a: () => {}, b: '123', c: {d: 'e'}, f: 3}, prevState: null, nextProps: {a: () => {}, b: '12345', c: {d: 'e'}, f: 3}, nextState: {a: 4}, }; const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, displayName: 'TestComponent', ownerDataMap: expect.any(WeakMap), reason: { propsDifferences: [ { pathString: 'a', diffType: diffTypes.function, prevValue: input.prevProps.a, nextValue: input.nextProps.a, }, { pathString: 'b', diffType: diffTypes.different, prevValue: input.prevProps.b, nextValue: input.nextProps.b, }, { pathString: 'c', diffType: diffTypes.deepEquals, prevValue: input.prevProps.c, nextValue: input.nextProps.c, }, ], stateDifferences: [ { pathString: 'a', diffType: diffTypes.different, prevValue: undefined, nextValue: input.nextState.a, }, ], hookDifferences: false, ownerDifferences: false, }, }); }); test('deep equals and same object', () => { const sameProp = {a: {b: 'c'}}; const prevProps = {className: 'aa', style: {width: '100%'}, sameProp}; const nextProps = {className: 'aa', style: {width: '100%'}, sameProp}; const input = getUpdateInfo({ Component: TestComponent, displayName: getDisplayName(TestComponent), prevProps, prevState: null, nextProps, nextState: null, }); const updateInfo = getUpdateInfo(input); expect(updateInfo).toEqual({ ...input, ownerDataMap: expect.any(WeakMap), displayName: 'TestComponent', reason: { propsDifferences: [ { pathString: 'style', diffType: diffTypes.deepEquals, prevValue: input.prevProps.style, nextValue: input.nextProps.style, }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }, }); }); }); ================================================ FILE: tests/hooks/childrenUsingHookResults.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; let updateInfos = []; // eslint-disable-next-line no-console const someFn = () => console.log('hi!'); beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { if (React.__REVERT_WHY_DID_YOU_RENDER__) { React.__REVERT_WHY_DID_YOU_RENDER__(); } }); describe('children using hook results', () => { test('without dependencies', () => { const AChild = () =>
    hi!
    ; AChild.whyDidYouRender = true; const ComponentWithMemoHook = () => { const [currentState, setCurrentState] = React.useState({c: 'c'}); React.useLayoutEffect(() => { setCurrentState({c: 'c'}); }, []); const fnUseCallback = React.useCallback(() => someFn(currentState.c)); const fnUseMemo = React.useMemo(() => () => someFn(currentState.c)); const fnRegular = () => someFn(currentState.c); return ( ); }; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: false, stateDifferences: false, propsDifferences: [ expect.objectContaining({ pathString: 'fnRegular', diffType: 'function', }), expect.objectContaining({ pathString: 'fnUseMemo', diffType: 'function', }), expect.objectContaining({ pathString: 'fnUseCallback', diffType: 'function', }), ], ownerDifferences: { hookDifferences: [ { differences: [ { diffType: 'deepEquals', nextValue: {c: 'c'}, pathString: '', prevValue: {c: 'c'}, }, ], hookName: 'useState', }, ], propsDifferences: false, stateDifferences: false, }, }); }); test('with different dependencies', () => { const Child = () =>
    hi!
    ; Child.whyDidYouRender = true; const ComponentWithMemoHook = () => { const [currentState, setCurrentState] = React.useState({c: 'c'}); React.useLayoutEffect(() => { setCurrentState({c: 'd'}); }, []); const fnUseCallback = React.useCallback(() => someFn(currentState.c), [currentState]); const fnUseMemo = React.useMemo(() => () => someFn(currentState.c), [currentState]); const fnRegular = () => someFn(currentState.c); return ( ); }; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: false, stateDifferences: false, propsDifferences: expect.arrayContaining([ expect.objectContaining({ pathString: 'fnRegular', diffType: diffTypes.function, }), expect.objectContaining({ pathString: 'fnUseMemo', diffType: diffTypes.different, }), expect.objectContaining({ pathString: 'fnUseCallback', diffType: diffTypes.different, }), expect.objectContaining({ pathString: 'fnUseMemo:parent-hook-useMemo-deps', diffType: diffTypes.different, }), expect.objectContaining({ pathString: 'fnUseCallback:parent-hook-useCallback-deps', diffType: diffTypes.different, }), ]), ownerDifferences: { hookDifferences: [ { differences: [ { diffType: diffTypes.different, pathString: '.c', prevValue: 'c', nextValue: 'd', }, { diffType: diffTypes.different, pathString: '', prevValue: {c: 'c'}, nextValue: {c: 'd'}, }, ], hookName: 'useState', }, ], propsDifferences: false, stateDifferences: false, }, }); }); test('with deep Equals dependencies', () => { const Child = () =>
    hi!
    ; Child.whyDidYouRender = true; const ComponentWithMemoHook = () => { const [currentState, setCurrentState] = React.useState({c: 'c'}); React.useLayoutEffect(() => { setCurrentState({c: 'c'}); }, []); const fnUseCallback = React.useCallback(() => someFn(currentState.c), [currentState]); const fnUseMemo = React.useMemo(() => () => someFn(currentState.c), [currentState]); const fnRegular = () => someFn(currentState.c); return ( ); }; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: false, stateDifferences: false, propsDifferences: expect.arrayContaining([ expect.objectContaining({ pathString: 'fnRegular', diffType: diffTypes.function, }), expect.objectContaining({ pathString: 'fnUseMemo', diffType: diffTypes.function, }), expect.objectContaining({ pathString: 'fnUseCallback', diffType: diffTypes.function, }), expect.objectContaining({ pathString: 'fnUseMemo:parent-hook-useMemo-deps', diffType: diffTypes.deepEquals, }), expect.objectContaining({ pathString: 'fnUseCallback:parent-hook-useCallback-deps', diffType: diffTypes.deepEquals, }), ]), ownerDifferences: { hookDifferences: [ { differences: [ { diffType: diffTypes.deepEquals, pathString: '', prevValue: {c: 'c'}, nextValue: {c: 'c'}, }, ], hookName: 'useState', }, ], propsDifferences: false, stateDifferences: false, }, }); }); }); ================================================ FILE: tests/hooks/hooks.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; describe('hooks - simple', () => { describe('hooks - track', () => { let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { if (React.__REVERT_WHY_DID_YOU_RENDER__) { React.__REVERT_WHY_DID_YOU_RENDER__(); } }); test('no whyDidYouRender=true', () => { const ComponentWithHooks = ({a}) => { const [currentState] = React.useState({b: 'b'}); return (
    hi! {a} {currentState.b}
    ); }; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(0); }); test('simple hooks tracking', () => { const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState({b: 'b'}); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(1); }); test('after removing WDYR', () => { React.__REVERT_WHY_DID_YOU_RENDER__(); const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState({b: 'b'}); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(0); }); test('track component', () => { const ComponentWithHooks = ({a}) => { const [currentState] = React.useState({b: 'b'}); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('track memoized component', () => { const ComponentWithHooks = React.memo(({a}) => { const [currentState] = React.useState({b: 'b'}); return (
    hi! {a} {currentState.b}
    ); }); ComponentWithHooks.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); }); describe('hooks - do not track', () => { let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), trackHooks: false, }); }); afterEach(() => { if (React.__REVERT_WHY_DID_YOU_RENDER__) { React.__REVERT_WHY_DID_YOU_RENDER__(); } }); test('no whyDidYouRender=true', () => { const ComponentWithHooks = ({a}) => { const [currentState] = React.useState({b: 'b'}); return (
    hi! {a} {currentState.b}
    ); }; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(0); }); test('with whyDidYouRender=true', () => { const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState({b: 'b'}); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(0); }); test('after removing WDYR', () => { React.__REVERT_WHY_DID_YOU_RENDER__(); const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState({b: 'b'}); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(0); }); }); }); ================================================ FILE: tests/hooks/useContext.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; describe('hooks - useContext', () => { let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('same value', () => { const MyContext = React.createContext('c'); const ComponentWithContextHook = ({a, b}) => { const valueFromContext = React.useContext(MyContext); return (
    hi! {a} {b} {valueFromContext}
    ); }; ComponentWithContextHook.whyDidYouRender = true; const OuterComponent = () => { const [currentState, setCurrentState] = React.useState('c'); React.useLayoutEffect(() => { setCurrentState('c'); }, []); return (
    ); }; rtl.render( ); expect(updateInfos).toHaveLength(0); }); test('deep equals - memoized', () => { const MyContext = React.createContext({c: 'c'}); const ComponentWithContextHook = React.memo(({a, b}) => { const valueFromContext = React.useContext(MyContext); return (
    hi! {a} {b} {valueFromContext.c}
    ); }); ComponentWithContextHook.whyDidYouRender = true; const OuterComponent = () => { const [currentState, setCurrentState] = React.useState({c: 'c'}); React.useLayoutEffect(() => { setCurrentState({c: 'c'}); }, []); return (
    ); }; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {c: 'c'}, prevValue: {c: 'c'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); }); test('deep equals - not memoized', () => { const MyContext = React.createContext({c: 'c'}); const ComponentWithContextHook = ({a, b}) => { const valueFromContext = React.useContext(MyContext); return (
    hi! {a} {b} {valueFromContext.c}
    ); }; ComponentWithContextHook.whyDidYouRender = true; const OuterComponent = () => { const [currentState, setCurrentState] = React.useState({c: 'c'}); React.useLayoutEffect(() => { setCurrentState({c: 'c'}); }, []); return (
    ); }; rtl.render( ); expect(updateInfos).toHaveLength(2); expect(updateInfos[0].reason).toEqual({ hookDifferences: false, propsDifferences: [], stateDifferences: false, ownerDifferences: { hookDifferences: [{ differences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {c: 'c'}, prevValue: {c: 'c'}, }], hookName: 'useState', }], propsDifferences: false, stateDifferences: false, }, }); expect(updateInfos[1]).toEqual(expect.objectContaining({ hookName: 'useContext', reason: { hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {c: 'c'}, prevValue: {c: 'c'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, } })); }); }); ================================================ FILE: tests/hooks/useReducer.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; describe('hooks - useReducer', () => { let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('same value', () => { const initialState = {b: 'b'}; function reducer() { return initialState; } let numOfRenders = 0; const ComponentWithHooks = () => { numOfRenders++; const [state, dispatch] = React.useReducer(reducer, initialState); React.useLayoutEffect(() => { dispatch({type: 'something'}); }, []); return (
    hi! {state.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(numOfRenders).toBe(2); expect(updateInfos).toHaveLength(0); }); test('different value', () => { const initialState = {b: 'b'}; function reducer() { return {a: 'a'}; } const ComponentWithHooks = ({a}) => { const [state, dispatch] = React.useReducer(reducer, initialState); React.useLayoutEffect(() => { dispatch({type: 'something'}); }, []); return (
    hi! {a} {state.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: [{ pathString: '', diffType: diffTypes.different, prevValue: {b: 'b'}, nextValue: {a: 'a'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); }); test('deep equals', () => { const initialState = {b: 'b'}; function reducer() { return {b: 'b'}; } const ComponentWithHooks = ({a}) => { const [state, dispatch] = React.useReducer(reducer, initialState); React.useLayoutEffect(() => { dispatch({type: 'something'}); }, []); return (
    hi! {a} {state.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {b: 'b'}, prevValue: {b: 'b'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); }); }); ================================================ FILE: tests/hooks/useState.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; describe('hooks - useState', () => { let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('setState - same value', () => { const initialState = {b: 'b'}; const ComponentWithHooks = () => { const [currentState, setCurrentState] = React.useState(initialState); React.useLayoutEffect(() => { setCurrentState(initialState); }, []); return (
    hi! {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(0); }); test('setState of different values', () => { const ComponentWithHooks = () => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState({b: 'c'}); }, []); return (
    hi! {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: false, stateDifferences: false, hookDifferences: [ { pathString: '.b', diffType: diffTypes.different, prevValue: 'b', nextValue: 'c', }, { pathString: '', diffType: diffTypes.different, prevValue: {b: 'b'}, nextValue: {b: 'c'}, }, ], ownerDifferences: false, }); }); test('setState of deep equals values', () => { const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState({b: 'b'}); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: false, stateDifferences: false, hookDifferences: [{ pathString: '', diffType: diffTypes.deepEquals, prevValue: {b: 'b'}, nextValue: {b: 'b'}, }], ownerDifferences: false, }); }); }); describe('track hooks', () => { let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { if (React.__REVERT_WHY_DID_YOU_RENDER__) { React.__REVERT_WHY_DID_YOU_RENDER__(); } }); test('same value', () => { const value = {b: 'b'}; let effectCalled = false; const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState(value); React.useLayoutEffect(() => { effectCalled = true; setCurrentState(value); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(0); expect(effectCalled).toBeTruthy(); }); test('different (falsy to truthy)', () => { const ComponentWithHooks = () => { const [currentResult, setCurrentState] = React.useState(false); const result = React.useMemo(() => currentResult, [currentResult]); React.useLayoutEffect(() => { setCurrentState(true); }, []); return (
    hi! {result}
    ); }; ComponentWithHooks.whyDidYouRender = { logOnDifferentValues: true, }; rtl.render( ); expect(updateInfos).toHaveLength(1); }); test('deep equals', () => { const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState({b: 'b'}); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {b: 'b'}, prevValue: {b: 'b'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); }); test('deep equals direct import', () => { const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState({b: 'b'}); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {b: 'b'}, prevValue: {b: 'b'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); }); test('many deep equals direct import', () => { const ComponentWithHooks = ({a}) => { const [currentStateA, setCurrentStateA] = React.useState({a: 'a'}); const [currentStateB, setCurrentStateB] = React.useState({b: 'b'}); const [currentStateC, setCurrentStateC] = React.useState({c: 'c'}); const [currentStateD, setCurrentStateD] = React.useState({d: 'd'}); const [currentStateE, setCurrentStateE] = React.useState({e: 'e'}); React.useLayoutEffect(() => { setCurrentStateA({a: 'a'}); setCurrentStateB({b: 'b'}); setCurrentStateC({c: 'c'}); setCurrentStateD({d: 'd'}); setCurrentStateE({e: 'e'}); }, []); return (
    hi! {a} {currentStateA.a} {currentStateB.b} {currentStateC.c} {currentStateD.d} {currentStateE.e}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(5); expect(updateInfos[0].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {a: 'a'}, prevValue: {a: 'a'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); expect(updateInfos[1].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {b: 'b'}, prevValue: {b: 'b'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); expect(updateInfos[2].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {c: 'c'}, prevValue: {c: 'c'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); expect(updateInfos[3].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {d: 'd'}, prevValue: {d: 'd'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); expect(updateInfos[4].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {e: 'e'}, prevValue: {e: 'e'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); }); test('deep equals functional use', () => { const ComponentWithHooks = ({a}) => { const [currentState, setCurrentState] = React.useState({b: 'b'}); React.useLayoutEffect(() => { setCurrentState(() => ({b: 'b'})); }, []); return (
    hi! {a} {currentState.b}
    ); }; ComponentWithHooks.whyDidYouRender = true; rtl.render( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: [{ diffType: diffTypes.deepEquals, pathString: '', nextValue: {b: 'b'}, prevValue: {b: 'b'}, }], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); }); }); ================================================ FILE: tests/hooks/useSyncExternalStore.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; describe('hooks - useSyncExternalStore', () => { let updateInfos = []; function createSimpleStore(initialState) { let state = initialState; const listeners = new Set(); return { getState: () => state, setState: (newState) => { state = newState; listeners.forEach((listener) => listener()); }, subscribe: (listener) => { listeners.add(listener); return () => listeners.delete(listener); }, }; } beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: (updateInfo) => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('same value', () => { const store = createSimpleStore('c'); const ComponentWithSyncExternalStore = ({a, b}) => { const valueFromStore = React.useSyncExternalStore( store.subscribe, store.getState ); return (
    hi! {a} {b} {valueFromStore}
    ); }; ComponentWithSyncExternalStore.whyDidYouRender = true; const OuterComponent = () => { React.useLayoutEffect(() => { store.setState('c'); }, []); return (
    ); }; rtl.render(); expect(updateInfos).toHaveLength(0); }); test('deep equals', () => { const store = createSimpleStore({c: 'c'}); const ComponentWithSyncExternalStore = ({a, b}) => { const valueFromStore = React.useSyncExternalStore( store.subscribe, store.getState ); return (
    hi! {a} {b} {valueFromStore.c}
    ); }; ComponentWithSyncExternalStore.whyDidYouRender = true; const OuterComponent = () => { React.useLayoutEffect(() => { store.setState({c: 'c'}); }, []); return (
    ); }; rtl.render(); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ hookDifferences: [ { diffType: diffTypes.deepEquals, pathString: '', nextValue: {c: 'c'}, prevValue: {c: 'c'}, }, ], propsDifferences: false, stateDifferences: false, ownerDifferences: false, }); }); }); ================================================ FILE: tests/index.test.js ================================================ import React from 'react'; import ReactDOMServer from 'react-dom/server'; import * as rtl from '@testing-library/react'; import _ from 'lodash'; import whyDidYouRender from '~'; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('dont swallow errors', () => { const BrokenComponent = React.memo(null); BrokenComponent.whyDidYouRender = true; const mountBrokenComponent = () => { rtl.render( ); }; expect(mountBrokenComponent).toThrow(/expected a string.*but got.*null/); global.flushConsoleOutput() .map(output => ({ ...output, args: output.args.map(a => _.isError(a) ? JSON.stringify(a.message) : a) })) .forEach(output => { expect(output).toEqual({ level: 'error', args: expect.arrayContaining([ expect.stringMatching(/(memo: The first argument must be a component|propTypes|error boundary)/), ]), }); }); }); test('render to static markup', () => { class MyComponent extends React.Component { static whyDidYouRender = true; render() { return (
    hi!
    ); } } const string = ReactDOMServer.renderToStaticMarkup(); expect(string).toBe('
    hi!
    '); }); ================================================ FILE: tests/librariesTests/react-redux.test.js ================================================ import React from 'react'; import {legacy_createStore as createStore} from 'redux'; import {cloneDeep} from 'lodash'; import * as rtl from '@testing-library/react'; import {diffTypes} from '~/consts'; import whyDidYouRender from '~'; const ReactRedux = {...require('react-redux')}; const {connect, Provider} = ReactRedux; describe('react-redux - simple', () => { const initialState = {a: {b: 'c'}}; const rootReducer = (state, action) => { if (action.type === 'differentState') { return {a: {b: 'd'}}; } if (action.type === 'deepEqlState') { return cloneDeep(state); } return state; }; let store; let updateInfos; beforeEach(() => { store = createStore(rootReducer, initialState); updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { if (React.__REVERT_WHY_DID_YOU_RENDER__) { React.__REVERT_WHY_DID_YOU_RENDER__(); } }); test('same state after dispatch', () => { const SimpleComponent = ({a}) => (
    {a.b}
    ); const ConnectedSimpleComponent = connect( state => ({a: state.a}) )(SimpleComponent); SimpleComponent.whyDidYouRender = true; const Main = () => ( ); rtl.render(
    ); expect(store.getState().a.b).toBe('c'); 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.render(
    ); expect(store.getState().a.b).toBe('c'); rtl.act(() => { store.dispatch({type: 'differentState'}); }); expect(store.getState().a.b).toBe('d'); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [ expect.objectContaining({diffType: diffTypes.different}), expect.objectContaining({diffType: diffTypes.different}), ], stateDifferences: false, hookDifferences: false, ownerDifferences: expect.anything(), }); }); test('deep equals state after dispatch', () => { const SimpleComponent = ({a}) => (
    {a.b}
    ); const ConnectedSimpleComponent = connect( state => ({a: state.a}) )(SimpleComponent); SimpleComponent.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: expect.anything(), }); }); }); describe('react-redux - hooks', () => { const initialState = {a: {b: 'c'}}; const rootReducer = (state, action) => { if (action.type === 'differentState') { return {a: {b: 'd'}}; } if (action.type === 'deepEqlState') { return cloneDeep(state); } return state; }; let store; let updateInfos; beforeEach(() => { store = createStore(rootReducer, initialState); updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), trackExtraHooks: [ [ReactRedux, 'useSelector'], ], }); }); afterEach(() => { if (React.__REVERT_WHY_DID_YOU_RENDER__) { React.__REVERT_WHY_DID_YOU_RENDER__(); } }); test('same state after dispatch', () => { const ConnectedSimpleComponent = () => { const a = ReactRedux.useSelector(state => state); return (
    {a.b}
    ); }; ConnectedSimpleComponent.whyDidYouRender = true; const Main = () => ( ); rtl.render(
    ); expect(store.getState().a.b).toBe('c'); rtl.act(() => { store.dispatch({type: 'sameState'}); }); expect(store.getState().a.b).toBe('c'); expect(updateInfos).toHaveLength(0); }); test('different state after dispatch', () => { const ConnectedSimpleComponent = () => { const a = ReactRedux.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: 'differentState'}); }); expect(store.getState().a.b).toBe('d'); expect(updateInfos).toHaveLength(1); expect(updateInfos[0]).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 = ReactRedux.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(store.getState().a.b).toBe('c'); expect(updateInfos).toHaveLength(1); expect(updateInfos[0]).toEqual(expect.objectContaining({ hookName: 'useSelector', reason: { propsDifferences: false, stateDifferences: false, hookDifferences: [ {diffType: diffTypes.deepEquals, pathString: '', prevValue: {b: 'c'}, nextValue: {b: 'c'}}, ], ownerDifferences: false, }, })); }); }); ================================================ FILE: tests/librariesTests/react-router-dom.test.js ================================================ import React from 'react'; import {legacy_createStore as createStore} from 'redux'; import { BrowserRouter, useLocation, Routes, Route, } from 'react-router-dom'; import {connect, Provider} from 'react-redux'; import {cloneDeep} from 'lodash'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); describe('react-router-dom', () => { test('simple', () => { const InnerComp = ({a}) => { const location = useLocation(); const [state, setState] = React.useState(0); React.useLayoutEffect(() => { setState(a => a + 1); }, []); // eslint-disable-next-line no-console console.log(`location is: ${location.pathname}`); return (
    hi! {JSON.stringify(a)} {state}
    ); }; InnerComp.whyDidYouRender = true; const Comp = () => ( }/> ); const {rerender} = rtl.render(); rerender(); const consoleOutputs = flushConsoleOutput(); expect(consoleOutputs).toEqual([ expect.objectContaining({args: ['location is: /']}), expect.objectContaining({args: ['location is: /']}), expect.objectContaining({args: ['location is: /']}), ]); expect(updateInfos).toHaveLength(2); expect(updateInfos).toEqual([ expect.objectContaining({ displayName: 'InnerComp', hookName: 'useState', }), expect.objectContaining({ displayName: 'InnerComp', reason: { hookDifferences: false, stateDifferences: false, propsDifferences: [{ diffType: 'deepEquals', nextValue: {b: 'c'}, pathString: 'a', prevValue: {b: 'c'}, }], ownerDifferences: { hookDifferences: false, propsDifferences: false, stateDifferences: false } } }) ]); }); test('with redux', () => { const initialState = {a: {b: 'c'}}; const rootReducer = (state, action) => { if (action.type === 'differentState') { return {a: {b: 'd'}}; } if (action.type === 'deepEqlState') { return cloneDeep(state); } return state; }; const store = createStore(rootReducer, initialState); const InnerFn = ({a, setDeepEqlState}) => { const location = useLocation(); React.useLayoutEffect(() => { setDeepEqlState(); }, []); // eslint-disable-next-line no-console console.log(`location is: ${location.pathname}`); return
    hi! {a.b}
    ; }; const InnerComp = connect( state => ({a: state.a}), {setDeepEqlState: () => ({type: 'deepEqlState'})} )(InnerFn); InnerFn.whyDidYouRender = true; const Comp = () => ( }/> ); rtl.render(); const consoleOutputs = flushConsoleOutput(); expect(consoleOutputs).toEqual([ expect.objectContaining({args: ['location is: /']}), expect.objectContaining({args: ['location is: /']}), ]); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ diffType: diffTypes.deepEquals, pathString: 'a', prevValue: {b: 'c'}, nextValue: {b: 'c'}, }], stateDifferences: false, hookDifferences: false, ownerDifferences: expect.anything(), }); }); }); ================================================ FILE: tests/librariesTests/styled-components.test.js ================================================ import React from 'react'; import styled from 'styled-components/dist/styled-components.js'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('simple styled-components', () => { const InnerComponent = () =>
    foobar
    ; const StyledInnerComponent = styled(InnerComponent)``; StyledInnerComponent.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('styled-components wrap of a memoized component', () => { const InnerComponent = React.memo(() =>
    foobar
    ); const StyledInnerComponent = styled(InnerComponent)``; StyledInnerComponent.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('styled-components with forward ref', () => { const InnerComponent = React.forwardRef((props, ref) =>
    foobar
    ); const Styled = styled(InnerComponent)``; Styled.whyDidYouRender = true; const Wrapper = (props) => { const ref = React.useRef(null); return ; }; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, hookDifferences: false, ownerDifferences: { hookDifferences: false, propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, }, }); }); test('styled-components with memoized forward ref', () => { const InnerComponent = React.memo( React.forwardRef((props, ref) =>
    foobar
    ) ); const StyledInnerComponent = styled(InnerComponent)``; StyledInnerComponent.whyDidYouRender = true; const Wrapper = (props) => { const ref = React.useRef(null); return ; }; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, hookDifferences: false, ownerDifferences: { hookDifferences: false, propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, }, }); }); ================================================ FILE: tests/logOnDifferentValues.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { include: [/.*/], logOnDifferentValues: true, notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('hook value change', () => { const Foo = React.memo(function Foo(props) { return (
    Foo {props.a.v}
    ); }); const App = React.memo(function App() { const [text, setText] = React.useState('Click me'); return (

    ); }); const {getByTestId} = rtl.render( ); const button = getByTestId('button'); rtl.fireEvent.click(button); expect(updateInfos).toEqual([ expect.objectContaining({displayName: 'App'}), expect.objectContaining({displayName: 'Foo'}), expect.objectContaining({displayName: 'Foo'}), ]); }); test('Non simple objects', () => { const Foo = React.memo(function Foo({error}) { return (

    {error.message}

    {error.stack}

    ); }); const App = React.memo(function App() { const [text, setText] = React.useState('Click me'); return (

    ); }); const {getByTestId} = rtl.render( ); const button = getByTestId('button'); rtl.fireEvent.click(button); expect(updateInfos[1].reason.propsDifferences[0]).toEqual( expect.objectContaining({diffType: 'deepEquals', 'pathString': 'error'}), ); }); ================================================ FILE: tests/logOwnerReasons.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { if (React.__REVERT_WHY_DID_YOU_RENDER__) { React.__REVERT_WHY_DID_YOU_RENDER__(); } }); function createOwners(Child) { const FunctionalOwner = () => ; class ClassOwner extends React.Component { state = {a: 1}; componentDidMount() { this.setState({a: 2}); } render() { return ; } } function HooksOwner() { /* eslint-disable no-unused-vars */ const [a, setA] = React.useState(1); const [b, setB] = React.useState(1); /* eslint-enable */ React.useEffect(() => { setA(2); setB(2); }, []); return ; } return {FunctionalOwner, ClassOwner, HooksOwner}; } function CloneOwner({children}) { const [, setA] = React.useState(1); const [, setB] = React.useState(1); React.useLayoutEffect(() => { setA(2); setB(2); }, []); return React.cloneElement(children); } describe('logOwnerReasons - function child', () => { const Child = () => null; Child.whyDidYouRender = true; const {FunctionalOwner, ClassOwner, HooksOwner} = createOwners(Child); test('owner props changed', () => { const {rerender} = rtl.render(); rerender(); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, }, }); }); test('owner state changed', () => { rtl.render(); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: false, stateDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], hookDifferences: false, }, }); }); test('owner hooks changed', () => { rtl.render(); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: false, stateDifferences: false, hookDifferences: [ { hookName: 'useState', differences: [{ pathString: '', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], }, { hookName: 'useState', differences: [{ pathString: '', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], }, ], }, }); }); test('owner state updated during render', () => { function DerivedStateOwner({ready}) { const [wasReady, setWasReady] = React.useState(ready); if (ready && !wasReady) { setWasReady(true); } return ; } const {rerender} = rtl.render(); rerender(); rerender(); expect(updateInfos).toHaveLength(2); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: [{ pathString: 'ready', diffType: diffTypes.different, prevValue: false, nextValue: true, }], stateDifferences: false, hookDifferences: [ { hookName: 'useState', differences: [{ pathString: '', diffType: diffTypes.different, prevValue: false, nextValue: true, }], }, ], }, }); expect(updateInfos[1].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: [{ pathString: 'ready', diffType: diffTypes.different, prevValue: true, nextValue: false, }], stateDifferences: false, hookDifferences: [{hookName: 'useState', differences: false}], }, }); }); test('owner uses cloneElement', () => { rtl.render(); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: false, stateDifferences: false, hookDifferences: [ { hookName: 'useState', differences: [{ pathString: '', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], }, { hookName: 'useState', differences: [{ pathString: '', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], }, ], }, }); }); }); describe('logOwnerReasons - class child', () => { class Child extends React.Component { static whyDidYouRender = true; render() { return null; } } const {FunctionalOwner, ClassOwner, HooksOwner} = createOwners(Child); test('owner props changed', () => { const {rerender} = rtl.render(); rerender(); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, }, }); }); test('owner state changed', () => { rtl.render(); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: false, stateDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], hookDifferences: false, }, }); }); test('owner hooks changed', () => { rtl.render(); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { propsDifferences: false, stateDifferences: false, hookDifferences: [ { hookName: 'useState', differences: [{ pathString: '', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], }, { hookName: 'useState', differences: [{ pathString: '', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], }, ], }, }); }); }); ================================================ FILE: tests/normalizeOptions.test.js ================================================ /* eslint-disable no-console */ import normalizeOptions from '~/normalizeOptions'; test('Empty options works', () => { const options = normalizeOptions(); expect(options.consoleLog).toBe(console.log); }); test('User can rewrite options', () => { const ownNotifier = () => {}; const userOptions = { notifier: ownNotifier, }; const options = normalizeOptions(userOptions); expect(options.notifier).toBe(ownNotifier); expect(options.consoleLog).toBe(console.log); }); ================================================ FILE: tests/patches/patchClassComponent.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import createReactClass from 'create-react-class'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; class TestComponent extends React.Component { static whyDidYouRender = true; render() { return
    hi!
    ; } } const createStateTestComponent = (initialState, newState) => { return class StateTestComponent extends React.Component { static whyDidYouRender = true; state = initialState; componentDidMount() { this.setState(newState); } render() { return
    hi!
    ; } }; }; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('Empty props and state', () => { const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('Same props', () => { const {rerender} = rtl.render( ); rerender( ); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(updateInfos).toHaveLength(1); }); test('Same state', () => { const StateTestComponent = createStateTestComponent({a: 1}, {a: 1}); rtl.render( ); return Promise.resolve() .then(() => { expect(updateInfos[0].reason).toEqual({ propsDifferences: false, stateDifferences: [], hookDifferences: false, ownerDifferences: false, }); expect(updateInfos).toHaveLength(1); }); }); test('Props change', () => { const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('With implemented "componentDidUpdate()"', () => { let innerComponentDidUpdateCalled = false; class OwnTestComponent extends React.Component { static whyDidYouRender = true; componentDidUpdate() { innerComponentDidUpdateCalled = true; } render() { return
    hi!
    ; } } const {rerender} = rtl.render( ); rerender( ); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(innerComponentDidUpdateCalled).toBe(true); expect(updateInfos).toHaveLength(1); }); test('With render as an arrow function', () => { class OwnTestComponent extends React.Component { static whyDidYouRender = true; componentDidMount() { this.setState({c: 'c'}); } render = () => { return
    hi!
    ; }; } const {rerender} = rtl.render( ); expect(updateInfos[0].reason).toEqual({ propsDifferences: false, stateDifferences: [{ diffType: diffTypes.different, nextValue: 'c', pathString: 'c', prevValue: undefined, }], hookDifferences: false, ownerDifferences: false, }); rerender( ); expect(updateInfos[1].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(updateInfos).toHaveLength(2); }); test('With render as a binded function', () => { class OwnTestComponent extends React.Component { static whyDidYouRender = true; constructor(props, context) { super(props, context); this.render = this.render.bind(this); } componentDidMount() { this.setState({c: 'c'}); } render() { return
    hi!
    ; } } const {rerender} = rtl.render( ); expect(updateInfos[0].reason).toEqual({ propsDifferences: false, stateDifferences: [{ diffType: diffTypes.different, nextValue: 'c', pathString: 'c', prevValue: undefined, }], hookDifferences: false, ownerDifferences: false, }); rerender( ); expect(updateInfos[1].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(updateInfos).toHaveLength(2); }); test('With implemented "componentDidUpdate()" with a snapshot - not tracked', () => { let resolve = false; class OwnTestComponent extends React.Component { getSnapshotBeforeUpdate() { return true; } componentDidUpdate(prevProps, prevState, snapshot) { resolve = snapshot; } render() { return
    hi!
    ; } } const {rerender} = rtl.render( ); rerender( ); expect(resolve).toBe(true); expect(updateInfos).toHaveLength(0); }); test('With implemented "componentDidUpdate()" with a snapshot', () => { let resolve = false; class OwnTestComponent extends React.Component { static whyDidYouRender = true; getSnapshotBeforeUpdate() { return true; } componentDidUpdate(prevProps, prevState, snapshot) { resolve = snapshot; } render() { return
    hi!
    ; } } const {rerender} = rtl.render( ); rerender( ); expect(resolve).toBe(true); expect(updateInfos).toHaveLength(1); }); test('Component created with "createReactClass"', () => { const CreateReactClassComponent = createReactClass({ displayName: 'Foo', render() { return
    hi!
    ; }, }); CreateReactClassComponent.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('Component created with "createReactClass" with implemented "componentDidUpdate()"', () => { let innerComponentDidUpdateCalled = false; const CreateReactClassComponent = createReactClass({ displayName: 'Foo', componentDidUpdate() { innerComponentDidUpdateCalled = true; }, render() { return
    hi!
    ; }, }); CreateReactClassComponent.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(innerComponentDidUpdateCalled).toBe(true); }); test('Element created with "createFactory"', () => { const TestComponentElementCreator = React.createFactory(TestComponent); const {rerender} = rtl.render( TestComponentElementCreator({a: 1}) ); rerender( TestComponentElementCreator({a: 1}) ); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(updateInfos).toHaveLength(1); }); test('Element created with "cloneElement"', () => { const testElement = ; const testElement2 = React.cloneElement(testElement); const {rerender} = rtl.render(testElement); rerender(testElement2); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('Several class components', () => { const {rerender} = rtl.render( <> ); rerender( <> ); expect(updateInfos).toHaveLength(3); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(updateInfos[1].reason).toEqual({ propsDifferences: [{ diffType: diffTypes.deepEquals, pathString: 'a', nextValue: {a: 'a'}, prevValue: {a: 'a'}, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(updateInfos[2].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); ================================================ FILE: tests/patches/patchForwardRefComponent.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('forward ref', () => { const content = 'My component!!!'; const MyComponent = React.forwardRef((props, ref) => { return
    {content}
    ; }); MyComponent.whyDidYouRender = true; let componentContentFromRef = null; let timesRefWasCalled = 0; const handleRef = ref => { if (!ref) { return; } timesRefWasCalled++; componentContentFromRef = ref.innerHTML; }; const {rerender} = rtl.render( ); rerender( ); expect(componentContentFromRef).toBe(content); expect(timesRefWasCalled).toBe(1); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('forward ref a memo component', () => { expect(() => { const content = 'My component!!!'; const MyComponent = React.forwardRef(React.memo((props, ref) => { return
    {content}
    ; }, () => true)); MyComponent.whyDidYouRender = true; let componentContentFromRef = null; let timesRefWasCalled = 0; const handleRef = ref => { if (!ref) { return; } timesRefWasCalled++; componentContentFromRef = ref.innerHTML; }; const {rerender} = rtl.render( ); rerender( ); expect(componentContentFromRef).toBe(content); expect(timesRefWasCalled).toBe(1); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }).toThrow(); global.flushConsoleOutput(); }); ================================================ FILE: tests/patches/patchFunctionalOrStrComponent.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; const FunctionalTestComponent = () => (
    hi!
    ); FunctionalTestComponent.whyDidYouRender = true; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('simple inline component', () => { const InlineComponent = () => (
    hi!
    ); InlineComponent.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('Several functional components', () => { const {rerender} = rtl.render( <> ); rerender( <> ); expect(updateInfos).toHaveLength(3); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(updateInfos[1].reason).toEqual({ propsDifferences: [{ diffType: diffTypes.deepEquals, pathString: 'a', nextValue: {a: 'a'}, prevValue: {a: 'a'}, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); expect(updateInfos[2].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); ================================================ FILE: tests/patches/patchMemoComponent.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import whyDidYouRender from '~'; import {diffTypes} from '~/consts'; const ReactMemoTestComponent = React.memo(() => (
    hi!
    )); ReactMemoTestComponent.whyDidYouRender = true; ReactMemoTestComponent.displayName = 'ReactMemoTestComponent'; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('Memoize text component', () => { const obj = {a: []}; const Svg = React.memo('svg'); Svg.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(0); }); test('Component memoized with React.memo - no change', () => { const obj = {a: []}; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(0); }); test('Component memoized with React.memo - different prop values', () => { const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.different, prevValue: 1, nextValue: 2, }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('Component memoized with React.memo - deep equal prop values', () => { const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('React.memo Component memoized with another React.memo - deep equal prop values', () => { const ReactSecondMemoComponent = React.memo(ReactMemoTestComponent); ReactSecondMemoComponent.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('memo a forward ref component', () => { const content = 'My component!!!'; const MyComponent = React.memo(React.forwardRef((props, ref) => { return
    {content}
    ; })); MyComponent.whyDidYouRender = true; let componentContentFromRef = null; let timesRefWasCalled = 0; const handleRef = ref => { if (!ref) { return; } timesRefWasCalled++; componentContentFromRef = ref.innerHTML; }; const {rerender} = rtl.render( ); rerender( ); expect(componentContentFromRef).toBe(content); expect(timesRefWasCalled).toBe(1); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('memo a class component', () => { class ClassComponent extends React.Component { render() { return
    hi!
    ; } } const MyComponent = React.memo(ClassComponent); MyComponent.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('memo a pure class component', () => { class ClassComponent extends React.PureComponent { render() { return
    hi!
    ; } } const MyComponent = React.memo(ClassComponent); MyComponent.whyDidYouRender = true; const {rerender} = rtl.render( ); rerender( ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); global.flushConsoleOutput(); }); ================================================ FILE: tests/shouldTrack.test.js ================================================ import React from 'react'; import shouldTrack from '~/shouldTrack'; import whyDidYouRender from '~'; class TrackedTestComponent extends React.Component { static whyDidYouRender = true; render() { return
    hi!
    ; } } class TrackedTestComponentNoHooksTracking extends React.Component { static whyDidYouRender = {trackHooks: false}; render() { return
    hi!
    ; } } class NotTrackedTestComponent extends React.Component { render() { return
    hi!
    ; } } class ExcludedTestComponent extends React.Component { static whyDidYouRender = false; render() { return
    hi!
    ; } } class PureComponent extends React.PureComponent { render() { return
    hi!
    ; } } const MemoComponent = React.memo(() => (
    hi!
    )); MemoComponent.displayName = 'MemoComponent'; describe('shouldTrack', () => { afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); describe('component changes', () => { test('Do not track not tracked component (default)', () => { whyDidYouRender(React); const isShouldTrack = shouldTrack(NotTrackedTestComponent, {isHookChange: false}); expect(isShouldTrack).toBe(false); }); test('Track tracked component', () => { whyDidYouRender(React); const isShouldTrack = shouldTrack(TrackedTestComponent, {isHookChange: false}); expect(isShouldTrack).toBe(true); }); test('Track included not tracked components', () => { whyDidYouRender(React, { include: [/TestComponent/], }); const isShouldTrack = shouldTrack(NotTrackedTestComponent, {isHookChange: false}); expect(isShouldTrack).toBe(true); }); test('Dont track components with whyDidYouRender=false', () => { whyDidYouRender(React, { include: [/ExcludedTestComponent/], }); const isShouldTrack = shouldTrack(ExcludedTestComponent, {isHookChange: false}); expect(isShouldTrack).toBe(false); }); test('Do not track not included not tracked components', () => { whyDidYouRender(React, { include: [/0/], }); const isShouldTrack = shouldTrack(NotTrackedTestComponent, {isHookChange: false}); expect(isShouldTrack).toBe(false); }); test('Do not track excluded tracked components', () => { whyDidYouRender(React, { exclude: [/TrackedTestComponent/], }); const isShouldTrack = shouldTrack(TrackedTestComponent, {isHookChange: false}); expect(isShouldTrack).toBe(false); }); test('Pure component', () => { whyDidYouRender(React, { trackAllPureComponents: true, }); const isShouldTrack = shouldTrack(PureComponent, {isHookChange: false}); expect(isShouldTrack).toBe(true); }); test('Memo component', () => { whyDidYouRender(React, { trackAllPureComponents: true, }); const isShouldTrack = shouldTrack(MemoComponent, {isHookChange: false}); expect(isShouldTrack).toBe(true); }); test('Pure component excluded', () => { whyDidYouRender(React, { trackAllPureComponents: true, exclude: [/PureComponent/], }); const isShouldTrack = shouldTrack(PureComponent, {isHookChange: false}); expect(isShouldTrack).toBe(false); }); test('Memo component excluded', () => { whyDidYouRender(React, { trackAllPureComponents: true, exclude: [/MemoComponent/], }); const isShouldTrack = shouldTrack(MemoComponent, {isHookChange: false}); expect(isShouldTrack).toBe(false); }); }); describe('hooks changes', () => { test('Do not track not tracked component (default)', () => { whyDidYouRender(React); const isShouldTrack = shouldTrack(NotTrackedTestComponent, {isHookChange: true}); expect(isShouldTrack).toBe(false); }); test('Track tracked component', () => { whyDidYouRender(React); const isShouldTrack = shouldTrack(TrackedTestComponent, {isHookChange: true}); expect(isShouldTrack).toBe(true); }); test('Do not track hook changes with "trackHooks: false"', () => { whyDidYouRender(React); const isShouldTrack = shouldTrack(TrackedTestComponentNoHooksTracking, {isHookChange: true}); expect(isShouldTrack).toBe(false); }); }); }); ================================================ FILE: tests/strictMode.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import {diffTypes} from '~/consts'; import whyDidYouRender from '~'; class TestComponent extends React.Component { static whyDidYouRender = true; render() { return
    hi!
    ; } } class PureTestComponent extends React.PureComponent { static whyDidYouRender = true; render() { return
    hi!
    ; } } const FunctionalTestComponent = () => (
    hi!
    ); FunctionalTestComponent.whyDidYouRender = true; FunctionalTestComponent.displayName = 'FunctionalTestComponent'; const FunctionalTestComponentWithHooks = () => { const [state1, setState1] = React.useState({count1: 1}); const [state2, setState2] = React.useState({count2: 2}); React.useLayoutEffect(() => { setState1({count1: 1}); setState2({count2: 2}); }, []); return (
    hi! {state1.count1} {state2.count2}
    ); }; FunctionalTestComponentWithHooks.whyDidYouRender = true; FunctionalTestComponentWithHooks.displayName = 'FunctionalTestComponentWithHooks'; const ReactMemoTestComponent = React.memo(() => (
    hi!
    )); ReactMemoTestComponent.whyDidYouRender = true; ReactMemoTestComponent.displayName = 'ReactMemoTestComponent'; let updateInfos = []; beforeEach(() => { updateInfos = []; whyDidYouRender(React, { notifier: updateInfo => updateInfos.push(updateInfo), logOwnerReasons: true, trackHooks: true, }); }); afterEach(() => { React.__REVERT_WHY_DID_YOU_RENDER__(); }); test('Strict mode- class component no props change', () => { const {rerender} = rtl.render(
    ); rerender(
    ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('Strict mode- class component props change', () => { const {rerender} = rtl.render(
    ); rerender(
    ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('Strict mode- pure class component no props change', () => { const {rerender} = rtl.render(
    ); rerender(
    ); expect(updateInfos).toHaveLength(0); }); test('Strict mode- pure class component props change', () => { const {rerender} = rtl.render(
    ); rerender(
    ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [ { pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }, ], stateDifferences: false, hookDifferences: false, ownerDifferences: false, }); }); test('Strict mode- functional component no props change', () => { const Main = props => { return (
    ); }; const {rerender} = rtl.render(
    ); rerender(
    ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [], stateDifferences: false, hookDifferences: false, ownerDifferences: { hookDifferences: false, propsDifferences: [], stateDifferences: false, }, }); }); test('Strict mode- functional component with props change', () => { const Main = props => { return (
    ); }; const {rerender} = rtl.render(
    ); rerender(
    ); expect(updateInfos).toHaveLength(1); expect(updateInfos[0].reason).toEqual({ propsDifferences: [{ diffType: diffTypes.deepEquals, pathString: 'a', prevValue: [], nextValue: [], }], stateDifferences: false, hookDifferences: false, ownerDifferences: { hookDifferences: false, propsDifferences: [{ pathString: 'a', diffType: diffTypes.deepEquals, prevValue: [], nextValue: [], }], stateDifferences: false, }, }); }); test('Strict mode- functional component with hooks no props change', () => { const Main = props => { return (
    ); }; rtl.render(
    ); expect(updateInfos).toHaveLength(2); expect(updateInfos[0].reason).toEqual({ propsDifferences: false, stateDifferences: false, hookDifferences: [ { diffType: diffTypes.deepEquals, pathString: '', nextValue: {count1: 1}, prevValue: {count1: 1}, }, ], ownerDifferences: false, }); expect(updateInfos[1].reason).toEqual({ propsDifferences: false, stateDifferences: false, hookDifferences: [ { diffType: diffTypes.deepEquals, pathString: '', nextValue: {count2: 2}, prevValue: {count2: 2}, }, ], ownerDifferences: false, }); }); test('Strict mode- functional component with hooks with props change', () => { const Main = props => { return (
    ); }; rtl.render(
    ); expect(updateInfos).toHaveLength(2); expect(updateInfos[0].reason).toEqual({ propsDifferences: false, stateDifferences: false, hookDifferences: [ { diffType: diffTypes.deepEquals, pathString: '', nextValue: {count1: 1}, prevValue: {count1: 1}, }, ], ownerDifferences: false, }); expect(updateInfos[1].reason).toEqual({ propsDifferences: false, stateDifferences: false, hookDifferences: [ { diffType: diffTypes.deepEquals, pathString: '', nextValue: {count2: 2}, prevValue: {count2: 2}, }, ], ownerDifferences: false, }); }); test('Strict mode- strict parent and child', () => { const App = React.memo(() => { const [whatever, setWhatever] = React.useState({a: 'b'}); const [whatever2, setWhatever2] = React.useState({a2: 'b2'}); const clickme = () => { setWhatever({a: 'b'}); setWhatever2({a2: 'b2'}); }; return (
    {whatever.a} {whatever2.a2}
    ); }); function Child() { return
    child
    ; } Child.whyDidYouRender = true; const StrictApp = () => ( ); const {getByText} = rtl.render(); const buttonReference = getByText('test'); rtl.act(() => { buttonReference.click(); }); rtl.act(() => { buttonReference.click(); }); rtl.act(() => { buttonReference.click(); }); const ownerDifferences = { hookDifferences: [ { hookName: 'useState', differences: [ { diffType: 'deepEquals', pathString: '', prevValue: {a: 'b'}, nextValue: {a: 'b'}, }, ], }, { hookName: 'useState', differences: [ { diffType: 'deepEquals', pathString: '', prevValue: {a2: 'b2'}, nextValue: {a2: 'b2'}, }, ], }, ], propsDifferences: false, stateDifferences: false, }; expect(updateInfos).toHaveLength(3); expect(updateInfos[0].reason.ownerDifferences).toEqual(ownerDifferences); expect(updateInfos[1].reason.ownerDifferences).toEqual(ownerDifferences); expect(updateInfos[2].reason.ownerDifferences).toEqual(ownerDifferences); }); ================================================ FILE: tests/utils.test.js ================================================ import React from 'react'; import * as rtl from '@testing-library/react'; import {checkIfInsideAStrictModeTree} from '~/utils'; describe('checkIfInsideAStrictModeTree', () => { test('class component', () => { let isStrictMode; class TestComponent extends React.Component { static whyDidYouRender = true; render() { isStrictMode = checkIfInsideAStrictModeTree(this); return
    hi!
    ; } } rtl.render(
    ); expect(isStrictMode).toBe(false); rtl.render( <>
    hiiiiiiiii
    ); expect(isStrictMode).toBe(true); }); test('pure class component', () => { let isStrictMode; class TestComponent extends React.PureComponent { static whyDidYouRender = true; render() { isStrictMode = checkIfInsideAStrictModeTree(this); return
    hi!
    ; } } rtl.render(
    ); expect(isStrictMode).toBe(false); rtl.render( <>
    hiiiiiiiii
    ); expect(isStrictMode).toBe(true); }); }); ================================================ FILE: tsconfig.json ================================================ { "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "babel.config.cjs", "tests/babel.config.cjs"], "exclude": ["node_modules"], "compilerOptions": { "baseUrl": ".", "allowJs": true, "noEmit": true, "esModuleInterop": true, "moduleResolution": "Node", "jsx": "react", "paths": { "~*": ["src*"], } }, } ================================================ FILE: tsx-test.tsx ================================================ /* eslint-disable no-unused-vars */ import './types' import React from 'react' import * as Redux from 'react-redux' import whyDidYouRender, { ExtraHookToTrack } from '.'; /* SHOULD ERROR because bad trackExtraHooks was provided (second argument should be string) */ whyDidYouRender(React, {trackExtraHooks: [[Redux, Redux.useSelector]]}); whyDidYouRender(React, {trackExtraHooks: [[Redux, 'useSelector']]}); interface Props { str: string } const FunctionalComponent: React.FC = ({str}) =>
    {str}
    FunctionalComponent.whyDidYouRender = true FunctionalComponent.whyDidYouRender = {collapseGroups: true} /* SHOULD ERROR because we use an unsupported whyDidYouRender prop */ FunctionalComponent.whyDidYouRender = {nonWDYRProp: true} /* SHOULD ERROR because whyDidYouRender shouldn't be a string */ FunctionalComponent.whyDidYouRender = 'a' const MemoFunctionalComponent = React.memo(({str}) =>
    {str}
    ) MemoFunctionalComponent.whyDidYouRender = true MemoFunctionalComponent.whyDidYouRender = {collapseGroups: true} /* SHOULD ERROR because we use an unsupported whyDidYouRender prop */ MemoFunctionalComponent.whyDidYouRender = {nonWDYRProp: true} /* SHOULD ERROR because whyDidYouRender shouldn't be a string */ MemoFunctionalComponent.whyDidYouRender = 'a' /* SHOULD ERROR because bad trackExtraHooks was provided (second argument should be string) */ FunctionalComponent.whyDidYouRender = {trackExtraHooks: [[Redux, Redux.useSelector]]} FunctionalComponent.whyDidYouRender = {trackExtraHooks: [[Redux, 'useSelector']]} class RegularClassComponent extends React.Component{ render(){ const {str} = this.props return (
    {str}
    ) } } class ClassComponentWithBooleanWDYR extends React.Component{ static whyDidYouRender = true render(){ const {str} = this.props return (
    {str}
    ) } } class ClassComponentWithObjWDYR extends React.Component{ static whyDidYouRender = {collapseGroups: true} render(){ const {str} = this.props return (
    {str}
    ) } } class ErroredClassComponentWithNonWDYRProp extends React.Component{ /* SHOULD ERROR because we use an unsupported whyDidYouRender prop */ static whyDidYouRender = {nonWDYRProp: 'a'} render(){ const {str} = this.props return (
    {str}
    ) } } class ErroredClassComponentWithStringWDYR extends React.Component{ /* SHOULD ERROR because whyDidYouRender shouldn't be a string */ static whyDidYouRender = 'a' render(){ const {str} = this.props return (
    {str}
    ) } } class ErrorousClassComponentWithTrackExtraHooks extends React.Component{ static whyDidYouRender = { collapseGroups: true, /* SHOULD ERROR because bad trackExtraHooks was provided (second argument should be string) */ trackExtraHooks: [[Redux, Redux.useSelector] as ExtraHookToTrack] } render(){ const {str} = this.props return (
    {str}
    ) } } class ClassComponentWithTrackExtraHooks extends React.Component{ static whyDidYouRender = { collapseGroups: true, trackExtraHooks: [[Redux, 'useSelector'] as ExtraHookToTrack] } render(){ const {str} = this.props return (
    {str}
    ) } } class PureClassComponentWithBooleanWDYR extends React.PureComponent{ static whyDidYouRender = true render(){ const {str} = this.props return (
    {str}
    ) } } class PureClassComponentWithObjWDYR extends React.PureComponent{ static whyDidYouRender = {collapseGroups: true} render(){ const {str} = this.props return (
    {str}
    ) } } class ErroredPureClassComponentWithNonWDYRProp extends React.PureComponent{ /* SHOULD ERROR because we use an unsupported whyDidYouRender prop */ static whyDidYouRender = {nonWDYRProp: 'a'} render(){ const {str} = this.props return (
    {str}
    ) } } class ErroredPureClassComponentWithStringWDYR extends React.PureComponent{ /* SHOULD ERROR because whyDidYouRender shouldn't be a string */ static whyDidYouRender = 'a' render(){ const {str} = this.props return (
    {str}
    ) } } ================================================ FILE: types.d.ts ================================================ import * as React from 'react'; export interface HookDifference { pathString: string; diffType: string; prevValue: any; nextValue: any; } export interface ReasonForUpdate { hookDifferences: HookDifference[]; propsDifferences: boolean; stateDifferences: boolean; } export interface UpdateInfo { Component: React.Component; displayName: string; prevProps: any; prevState: any; nextProps: any; nextState: any; prevHookResult: any; nextHookResult: any; reason: ReasonForUpdate; options: WhyDidYouRenderOptions; hookName?: string; } export type ExtraHookToTrack = [any, string]; export interface WhyDidYouRenderOptions { include?: RegExp[]; exclude?: RegExp[]; trackAllPureComponents?: boolean; trackHooks?: boolean; logOwnerReasons?: boolean; trackExtraHooks?: Array; logOnDifferentValues?: boolean; hotReloadBufferMs?: number; onlyLogs?: boolean; collapseGroups?: boolean; titleColor?: string; diffNameColor?: string; diffPathColor?: string; textBackgroundColor?: string; notifier?: Notifier; customName?: string; } export type WhyDidYouRenderComponentMember = WhyDidYouRenderOptions | boolean export type Notifier = (options: UpdateInfo) => void declare function whyDidYouRender(react: typeof React, options?: WhyDidYouRenderOptions): typeof React; declare namespace whyDidYouRender { export const defaultNotifier: Notifier; } export default whyDidYouRender; declare module 'react' { interface FunctionComponent

    { whyDidYouRender?: WhyDidYouRenderComponentMember; } interface VoidFunctionComponent

    { whyDidYouRender?: WhyDidYouRenderComponentMember; } interface ExoticComponent

    { whyDidYouRender?: WhyDidYouRenderComponentMember; } namespace Component { const whyDidYouRender: WhyDidYouRenderComponentMember; } /* not supported. see https://github.com/microsoft/TypeScript/issues/33892 and https://github.com/microsoft/TypeScript/issues/34516 and https://github.com/microsoft/TypeScript/issues/32185 // interface Component

    extends ComponentLifecycle { // static whyDidYouRender?: WhyDidYouRenderComponentMember; // } */ }