Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 19 #301

Merged
merged 25 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fcc8a8f
update packages, remove react-router 5 tests
vzaidman Apr 28, 2024
45fcdcf
fixed access to React OwnerInstance
vzaidman Sep 15, 2024
cd969a0
test only latest react router dom
vzaidman Sep 15, 2024
1f524c0
fix jest polyfills
vzaidman Sep 15, 2024
7b6fc5c
update isReactElement to comply with React 19
vzaidman Sep 15, 2024
8cb3d36
fixed error reported when component fails to mount
vzaidman Sep 15, 2024
0b3cdd9
fix detection of current processed component
vzaidman Sep 15, 2024
0613270
improved detection of processing hooks in a new element
vzaidman Dec 28, 2024
cdd84dc
improve get owner reasons
vzaidman Dec 28, 2024
7325c08
fix forward ref tests
vzaidman Dec 28, 2024
5fbdd06
fix react-redux and improve tests
vzaidman Dec 28, 2024
2eb3f93
improve owner changes calculations
vzaidman Dec 28, 2024
943ebe5
update readme
vzaidman Sep 15, 2024
6bd8f0c
fixed nollup running the demo app + migrated from react-hot-loader to…
vzaidman Dec 28, 2024
db2914c
fix eslint
vzaidman Dec 29, 2024
3fb801c
remove older unused packages and resolutions
vzaidman Dec 29, 2024
6c617d6
fixed missing owner display name printing
vzaidman Dec 29, 2024
5e927f2
fixed ssr example
vzaidman Dec 29, 2024
1562bfc
quick fix to dark mode support
vzaidman Dec 29, 2024
39f6540
updated readme to link to older versions of React
vzaidman Dec 29, 2024
8cbb1c1
removed old resolution hint about eslint being on version 8
vzaidman Dec 29, 2024
7eed9c5
eliminated forward ref warnings
vzaidman Dec 29, 2024
1fb49ff
fix cypress ci
vzaidman Jan 18, 2025
395d14d
fix for react native automatic jsx
vzaidman Jan 18, 2025
cad504a
updated readme
vzaidman Jan 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

50 changes: 0 additions & 50 deletions .eslintrc

This file was deleted.

10 changes: 0 additions & 10 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ jobs:
- name: Run Cypress tests
run: yarn cypress:ci

cypress-tests-classic:
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- name: Run Cypress tests
run: yarn cypress:ci:classic

unit-tests:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"eslint.alwaysShowStatus": true,
"eslint.format.enable": true,
"eslint.codeActionsOnSave.mode": "problems",
"editor.formatOnSave": true,

"flow.enabled": false,

Expand All @@ -19,6 +20,7 @@

"jestrunner.debugOptions": {"args": ["--watch"]},
"jestrunner.configPath": "jest.config.js",

"cSpell.words": [
"astring",
"lcov",
Expand Down
60 changes: 39 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,36 @@

`why-did-you-render` by [Welldone Software](https://welldone.software/) monkey patches **`React`** to notify you about potentially avoidable re-renders. (Works with **`React Native`** as well.)

For example, if you pass `style={{width: '100%'}}` to a big pure component it would always re-render on every element creation:
For example, if you pass `style={{width: '100%'}}` to a big memo component it would always re-render on every element creation:
```jsx
<BigListPureComponent style={{width: '100%'}}/>
<MemoBigList style={{width: '100%'}}/>
```

It can also help you to simply track when and why a certain component re-renders.

> [!CAUTION]
> The library was not tested with [React Compiler](https://react.dev/learn/react-compiler) at all. I believe it's completely incompatible with it.

> [!CAUTION]
> Not all re-renders are *"bad"*. Sometimes shenanigan to reduce re-renders can either hurt your App's performance or have a neglagable effect, in which case it would be just a waste of your efforts, and complicate your code. Try to focus on heavier components when optimizing and use the [React profiler](https://legacy.reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html) inside the React dev-tools to measure the effects of any changes.

> [!NOTE]
I've joined the React team, specifically working on React tooling. This role has opened up exciting opportunities to enhance the developer experience for React users— and your input could offer valuable insights to help me with this effort. Please join the conversation in the [discussion thread](https://github.com/welldone-software/why-did-you-render/discussions/309)!

## Setup
The latest version of the library was tested [(unit tests and E2E)]((https://travis-ci.com/welldone-software/why-did-you-render.svg?branch=master)) with **`React@18`** only. For React 17 and 16, please use version @^7.
The latest version of the library was tested [(unit tests and E2E)]((https://travis-ci.com/welldone-software/why-did-you-render.svg?branch=master)) with **`React@19`** only.
* [For `React 18`, please see the readme for version @^8](https://github.com/welldone-software/why-did-you-render/tree/version-8).
* [For `React 17` and `React 16`, please see the readme for version @^7](https://github.com/welldone-software/why-did-you-render/tree/version-7).

```
npm install @welldone-software/why-did-you-render --save-dev
```
or
```
yarn add --dev @welldone-software/why-did-you-render
yarn add @welldone-software/why-did-you-render -D
```
Set the library to be the React's importSource and make sure `preset-react` is in `development` mode.

If you use the `automatic` JSX transformation, set the library to be the import source, and make sure `preset-react` is in `development` mode.
This is because `React 19` requires using the `automatic` [JSX transformation](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html).
```js
['@babel/preset-react', {
runtime: 'automatic',
Expand All @@ -43,15 +54,19 @@ If you use the `automatic` JSX transformation, set the library to be the import

#### Bare workflow

Unfortunately, the `metro-react-native-babel-preset` that comes with react-native out of the box does not allow you to change the options of the `babel/plugin-transform-react-jsx` plugin. Just add the plugin with options as listed below and start react-native packager as usual. Default env for babel is "development". If you do not use expo when working with react-native, the following method will help you.
Add the plugin as listed below and start react-native packager as usual. Default env for babel is "development". If you do not use expo when working with react-native, the following method will help you.

```js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],

env: {
development: {
plugins: [['@babel/plugin-transform-react-jsx', { runtime: 'classic' }]],
plugins: [['@babel/plugin-transform-react-jsx', {
runtime: 'automatic',
development: process.env.NODE_ENV === 'development',
importSource: '@welldone-software/why-did-you-render',
}]],
},
},
}
Expand All @@ -78,10 +93,10 @@ module.exports = function (api) {
};
```

> Notice: Create React App (CRA) ^4 **does use the `automatic` JSX transformation.**
> Notice: Create React App (CRA) ^4 **uses the `automatic` JSX transformation.**
> [See the following comment on how to do this step with CRA](https://github.com/welldone-software/why-did-you-render/issues/154#issuecomment-773905769)

Create a `wdyr.js` file and import it as **the first import** in your application.
Create a `wdyr.js` file and import it as **the very first import** in your application.

`wdyr.js`:
```jsx
Expand All @@ -95,35 +110,36 @@ if (process.env.NODE_ENV === 'development') {
}
```

> **Notice: The library should *NEVER* be used in production because it slows down React**
> [!CAUTION]
> The library should *NEVER* be used in production because:
> - It significantly slows down React
> - It monkey patches React and can result in unexpected behavior

In [Typescript](https://github.com/welldone-software/why-did-you-render/issues/161), call the file wdyr.ts and add the following line to the top of the file to import the package's types:
```tsx
/// <reference types="@welldone-software/why-did-you-render" />
```

Import `wdyr` as the first import (even before `react-hot-loader`):
Import `wdyr` as the first import (even before `react-hot-loader` if you use it):

`index.js`:

```jsx
import './wdyr'; // <--- first import

import 'react-hot-loader';
import {hot} from 'react-hot-loader/root';

import React from 'react';
import ReactDOM from 'react-dom';
// ...
import {App} from './app';
// ...
const HotApp = hot(App);
// ...
ReactDOM.render(<HotApp/>, document.getElementById('root'));
ReactDOM.render(<App/>, document.getElementById('root'));
```

If you use `trackAllPureComponents` like we suggest, all pure components ([React.PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent) or [React.memo](https://reactjs.org/docs/react-api.html#reactmemo)) will be tracked.
If you use `trackAllPureComponents`, all pure components ([React.PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent) or [React.memo](https://reactjs.org/docs/react-api.html#reactmemo)) will be tracked.

Otherwise, add `whyDidYouRender = true` to component classes/functions you want to track. (f.e `Component.whyDidYouRender = true`)
Otherwise, add `whyDidYouRender = true` to ad-hoc components to track them. (f.e `Component.whyDidYouRender = true`)

More information about what is tracked can be found in [Tracking Components](#tracking-components).

Expand Down Expand Up @@ -241,7 +257,8 @@ Optionally you can pass in `options` as the second parameter. The following opti
- `titleColor`
- `diffNameColor`
- `diffPathColor`
- `notifier: ({Component, displayName, hookName, prevProps, prevState, prevHook, nextProps, nextState, nextHook, reason, options, ownerDataMap}) => void`
- `textBackgroundColor`
- `notifier: ({Component, displayName, hookName, prevProps, prevState, prevHookResult, nextProps, nextState, nextHookResult, reason, options, ownerDataMap}) => void`
- `getAdditionalOwnerData: (element) => {...}`

#### include / exclude
Expand Down Expand Up @@ -283,7 +300,7 @@ whyDidYouRender(React, {
});
```

> There is currently a problem with rewriting exports of imported files in webpack. A workaround is available here: [#85 - trackExtraHooks cannot set property](https://github.com/welldone-software/why-did-you-render/issues/85)
> This feature is rewriting exports of imported files. There is currently a problem with that approach in webpack. A workaround is available here: [#85 - trackExtraHooks cannot set property](https://github.com/welldone-software/why-did-you-render/issues/85)

#### logOwnerReasons
##### (default: `true`)
Expand Down Expand Up @@ -325,10 +342,11 @@ If you don't want to use `console.group` to group logs you can print them as sim

Grouped logs can be collapsed.

#### titleColor / diffNameColor / diffPathColor
#### titleColor / diffNameColor / diffPathColor / textBackgroundColor
##### (default titleColor: `'#058'`)
##### (default diffNameColor: `'blue'`)
##### (default diffPathColor: `'red'`)
##### (default textBackgroundColor: `'white`)

Controls the colors used in the console notifications

Expand Down
10 changes: 4 additions & 6 deletions babel.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const compact = require('lodash/compact');
module.exports = function(api) {
const isProd = process.env.NODE_ENV === 'production';
const isTest = process.env.NODE_ENV === 'test';
const isUseClassicJSX = process.env.USE_CLASSIC_JSX === 'true';

api.cache(false);

Expand All @@ -12,16 +11,15 @@ module.exports = function(api) {
modules: isTest ? 'commonjs' : false,
}],
['@babel/preset-react', {
runtime: isUseClassicJSX ? 'classic' : 'automatic',
runtime: 'automatic',
development: true,
importSource: isUseClassicJSX ? undefined : `${__dirname}`,
importSource: `${__dirname}`,
}],
];

const plugins = compact([
(!isProd && !isTest) && 'react-hot-loader/babel',
!isProd && '@babel/plugin-transform-class-properties',
(!isProd && !isTest) && 'react-refresh/babel',
]);

return { presets, plugins };
return {presets, plugins};
};
6 changes: 3 additions & 3 deletions cypress/e2e/big_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ it('Big list basic example', () => {
cy.contains('button', 'Increase!').click();

expect(console.group).to.be.calledWithMatches([
{ match: 'BigList', times: 1 },
{ match: /props.*style\W/, times: 1 },
{match: 'BigList', times: 1},
{match: /props.*style\W/, times: 1},
]);

expect(console.log).to.be.calledWithMatches([
{ match: [() => true, 'Re-rendered because of props changes'], times: 1 },
{match: [() => true, 'Re-rendered because of props changes'], times: 1},
]);
});
});
6 changes: 3 additions & 3 deletions cypress/e2e/child-of-pure-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ it('Child of Pure Component', () => {
cy.contains('button', 'clicks:').should('contain', '2');

expect(console.group).to.be.calledWithMatches([
{ match: 'PureFather', times: 2 },
{ match: /props.*children\W/, times: 2 },
{match: 'PureFather', times: 2},
{match: /props.*children\W/, times: 2},
]);

expect(console.log).to.be.calledWithMatches([
{ match: 'syntax always produces a *NEW* immutable React element', times: 2 },
{match: 'syntax always produces a *NEW* immutable React element', times: 2},
]);
});
});
4 changes: 2 additions & 2 deletions cypress/e2e/clone-element.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
it('Creating react element using React.cloneElement', () => {
cy.visitAndSpyConsole('/#cloneElement', console => {
expect(console.group).to.be.calledWithMatches([
{ match: 'TestComponent', times: 1 },
{match: 'TestComponent', times: 1},
]);

expect(console.log).to.be.calledWithMatches([
{ match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1 },
{match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1},
]);
});
});
4 changes: 2 additions & 2 deletions cypress/e2e/create-factory.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
it('Creating react element using React.createFactory', () => {
cy.visitAndSpyConsole('/#createFactory', console => {
expect(console.group).to.be.calledWithMatches([
{ match: 'TestComponent', times: 1 },
{match: 'TestComponent', times: 1},
]);

expect(console.log).to.be.calledWithMatches([
{ match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1 },
{match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1},
]);
});
});
14 changes: 7 additions & 7 deletions cypress/e2e/hooks-use-context.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
it('Hooks - useContext', () => {
cy.visitAndSpyConsole('/#useContext', console => {
expect(console.group).to.be.calledWithMatches([
{ match: /ComponentWithContextHook$/, times: 2 },
{ match: 'Rendered by Main', times: 1 },
{ match: 'ComponentWithContextHookInsideMemoizedParent', times: 1 },
{ match: '[hook useState result]', times: 1 },
{ match: '[hook useContext result]', times: 2 },
{match: /ComponentWithContextHook$/, times: 2},
{match: 'Rendered by Main', times: 1},
{match: 'ComponentWithContextHookInsideMemoizedParent', times: 1},
{match: '[hook useState result]', times: 1},
{match: '[hook useContext result]', times: 2},
]);

expect(console.log).to.be.calledWithMatches([
{ match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1 },
{ match: [() => true, 'Re-rendered because of hook changes'], times: 3 },
{match: [() => true, 'Re-rendered because the props object itself changed but its values are all equal.'], times: 1},
{match: [() => true, 'Re-rendered because of hook changes'], times: 3},
]);
});
});
8 changes: 4 additions & 4 deletions cypress/e2e/hooks-use-memo-and-callback-child.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ it('Hooks - useMemo and useCallback Child', () => {
cy.contains('button', 'count: 0').click();

expect(console.group).to.be.calledWithMatches([
{ match: 'Comp', times: 2 },
{ match: /useMemoFn/, times: 2 },
{ match: /useCallbackFn/, times: 2 },
{ match: /props.*\..*count/, times: 1 },
{match: 'Comp', times: 2},
{match: /useMemoFn/, times: 2},
{match: /useCallbackFn/, times: 2},
{match: /props.*\..*count/, times: 1},
]);
});
});
6 changes: 3 additions & 3 deletions cypress/e2e/hooks-use-reducer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
it('Hooks - useReducer', () => {
const checkConsole = (console, times) => {
expect(console.group).to.be.calledWithMatches([
{ match: 'Main', times },
{ match: '[hook useReducer result]', times },
{match: 'Main', times},
{match: '[hook useReducer result]', times},
]);

expect(console.log).to.be.calledWithMatches([
{ match: 'different objects that are equal by value.', times },
{match: 'different objects that are equal by value.', times},
]);
};

Expand Down
4 changes: 2 additions & 2 deletions cypress/e2e/hooks-use-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ it('Hooks - useState', () => {
});

expect(console.group).to.be.calledWithMatches([
{ match: 'BrokenHooksPureComponent', times: 2 },
{ match: '[hook useState result]', times: 2 },
{match: 'BrokenHooksPureComponent', times: 2},
{match: '[hook useState result]', times: 2},
]);
});
});
Loading
Loading