);
});
ComponentWithNewResultsForDeepEqualsDeps.displayName = 'ComponentWithNewResultsForDeepEqualsDeps';
function Main() {
const [count, setCount] = React.useState(0);
return (
setCount(count + 1)}>
Current count: {count}
);
}
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 (
);
}
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}'}
setNumObj({num: 0})}>
Will Cause a Re-render: {numObj.num}
>
);
}
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'}
setNum(0)}>
Will NOT Cause a Re-render: {num}
>
);
}
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'}
setNumObj({num: 0})}>
Will NOT Cause a Re-render: {numObj.num}
>
);
}
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}`}
dispatch({type: 'sameObj'})}>Same State
dispatch({type: 'deepEqlObj'})}>Deep Equal State
dispatch({type: 'randomObj'})}>Random Object
);
};
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}`}
Same State
Deep Equal State
Random Object
);
};
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 (
setText(state => state + '.')}
data-testid="button"
>
{text}
);
});
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 (
setText(state => state + '.')}
data-testid="button"
>
{text}
);
});
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 (
test
{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;
// }
*/
}