├── .changeset └── config.json ├── .eslintignore ├── .eslintrc.yml ├── .github ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── benchmarking.png ├── benchmark └── index.js ├── index.d.ts ├── index.js ├── package.json ├── react-fast-compare-Hero.png ├── test ├── .eslintrc.yml ├── browser │ ├── browser.spec.js │ ├── index.js │ ├── karma.conf.ie.js │ └── karma.conf.js ├── mocha.opts ├── node │ ├── advanced.spec.js │ ├── basics.spec.js │ └── tests.js └── typescript │ ├── sample-react-redux-usage.tsx │ ├── sample-usage.tsx │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { 6 | "repo": "FormidableLabs/react-fast-compare" 7 | } 8 | ], 9 | "access": "public", 10 | "baseBranch": "master" 11 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/typescript/index.js 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | node: true 4 | rules: 5 | block-scoped-var: 2 6 | callback-return: 2 7 | curly: [2, multi-or-nest, consistent] 8 | dot-location: [2, property] 9 | dot-notation: 2 10 | indent: [2, 2, { SwitchCase: 1 }] 11 | linebreak-style: [2, unix] 12 | no-else-return: 2 13 | no-eq-null: 2 14 | no-fallthrough: 2 15 | no-path-concat: 2 16 | no-return-assign: 2 17 | no-trailing-spaces: 2 18 | no-unused-vars: [2, { args: none }] 19 | no-use-before-define: [2, nofunc] 20 | quotes: [2, single, avoid-escape] 21 | semi: [2, always] 22 | strict: 0 23 | valid-jsdoc: [2, { requireReturn: false }] 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Description 9 | 10 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. 11 | 12 | ## Checklist: 13 | 14 | - [ ] All tests are passing 15 | - [ ] Type definitions, if updated, pass both `test-ts-defs` and `test-ts-usage` 16 | - [ ] Benchmark performance has not significantly decreased 17 | - [ ] Bundle size has not been significantly impacted 18 | - [ ] The bundle size badge has been updated to reflect the new size 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | types: [opened, synchronize, reopened] 10 | 11 | jobs: 12 | test: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: 17 | - ubuntu-latest 18 | - windows-latest 19 | node_version: 20 | - 18 21 | name: Node ${{ matrix.node_version }} on ${{ matrix.os }} 22 | steps: 23 | - name: Use LF EOL 24 | if: ${{ matrix.os == 'windows-latest' }} 25 | run: | 26 | git config --global core.autocrlf false 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | - name: Setup node 30 | uses: actions/setup-node@v4 31 | with: 32 | cache: "yarn" 33 | node-version: ${{ matrix.node_version }} 34 | - name: Install Dependencies 35 | run: yarn install --frozen-lockfile 36 | - name: Test 37 | run: yarn test 38 | - name: Codecov 39 | run: yarn codecov 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | id-token: write 13 | issues: write 14 | repository-projects: write 15 | deployments: write 16 | packages: write 17 | pull-requests: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | cache: "yarn" 23 | node-version: 18 24 | 25 | - name: Install dependencies 26 | run: yarn install --frozen-lockfile 27 | 28 | - name: Unit Tests 29 | run: yarn test 30 | 31 | - name: PR or Publish 32 | id: changesets 33 | uses: changesets/action@v1 34 | with: 35 | version: yarn changeset version 36 | publish: yarn changeset publish 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Compiled output from Typescript usage test 43 | test/typescript/*.js 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # npm lock file 61 | package-lock.json 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # editors 67 | .vscode/ 68 | 69 | .DS_Store 70 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.2.2 4 | 5 | ### Patch Changes 6 | 7 | - Adding GitHub release workflow ([#126](https://github.com/FormidableLabs/react-fast-compare/pull/126)) 8 | 9 | ## 3.2.1 (2023-03-16) 10 | 11 | **Bugfixes:** 12 | 13 | - Fix Object with null prototype errors [#64](https://github.com/FormidableLabs/react-fast-compare/issues/64). 14 | 15 | ## 3.2.0 (2020-05-28) 16 | 17 | - [#80](https://github.com/FormidableLabs/react-fast-compare/pull/80). Update types to use generic `any`s. 18 | - [#77](https://github.com/FormidableLabs/react-fast-compare/pull/77). Add tests for our TypeScript type definitions. 19 | 20 | ## 3.1.0 (2020-05-08) 21 | 22 | - [#76](https://github.com/FormidableLabs/react-fast-compare/pull/76). Add support for preact/compat. 23 | - [#75](https://github.com/FormidableLabs/react-fast-compare/pull/75). Drop test support for Node 8. 24 | - [#62](https://github.com/FormidableLabs/react-fast-compare/pull/62). Fix TypeScript types by declaring a function instead of a module. 25 | 26 | ## 3.0.2 (2020-05-01) 27 | 28 | - [#71](https://github.com/FormidableLabs/react-fast-compare/pull/71). Extend the `hasArrayBuffer` check to support older IE 11 versions. 29 | 30 | ## 3.0.1 (2020-02-05) 31 | 32 | - [#60](https://github.com/FormidableLabs/react-fast-compare/pull/60). Update documentation on bundle size. 33 | 34 | ## 3.0.0 (2020-01-05) 35 | 36 | **Features:** 37 | 38 | - [#36](https://github.com/FormidableLabs/react-fast-compare/pull/36). Update to `fast-deep-equal@3.1.1` with modified support for ES.next data types: `Map`, `Set`, `ArrayBuffer`. 39 | - [#57](https://github.com/FormidableLabs/react-fast-compare/pull/57). Minor refactoring to reduce min+gz size. 40 | - [#59](https://github.com/FormidableLabs/react-fast-compare/pull/59). Rename exported to `isEqual` for TypeScript users. 41 | 42 | **Breaking changes:** 43 | 44 | - instances of different classes are now considered unequal 45 | - support for ES6 Map and Set instances 46 | - support for ES6 typed arrays 47 | 48 | **Infrastructure:** 49 | 50 | - Upgrade lots of `devDependenices` 51 | - Use `fast-deep-equal` tests directly in our correctness tests. 52 | - Update CI to modern Node.js versions. 53 | - Update Appveyor to use straight IE11 (not emulated IE9) because mocha no longer runs in IE9. 54 | 55 | ## 2.0.4 (2018-11-09) 56 | 57 | - [#39](https://github.com/FormidableLabs/react-fast-compare/pull/39). Fix `react-native` bug introduced by DOM element checking. 58 | 59 | ## 2.0.3 (2018-11-08) 60 | 61 | - [#33](https://github.com/FormidableLabs/react-fast-compare/pull/33). Add handling for DOM elements. Thanks @viper1104! 62 | 63 | ## 2.0.2 (2018-08-21) 64 | 65 | - [#28](https://github.com/FormidableLabs/react-fast-compare/pull/28). Fix for localized versions of IE11. Thanks @excentrik! 66 | - [#34](https://github.com/FormidableLabs/react-fast-compare/pull/34). Fix typo. Thanks @Marviel! 67 | 68 | ## 2.0.1 (2018-06-25) 69 | 70 | - [#26](https://github.com/FormidableLabs/react-fast-compare/pull/26). Remove `_store` check. Thanks @chen-ye! 71 | 72 | **Major bugfix:** Fixes `RangeError` in production, [#25](https://github.com/FormidableLabs/react-fast-compare/issues/25) 73 | 74 | ## 2.0.0 (2018-06-04) 75 | 76 | - [#21](https://github.com/FormidableLabs/react-fast-compare/pull/21). Upgrade to `fast-deep-equal@2.0.1`. Thanks @samwhale! 77 | 78 | **Breaking changes:** 79 | 80 | - `null` and `Object` comparison 81 | - new behavior: functions are no longer treated as equal 82 | - new behavior: handle `NaN` 83 | 84 | ## 1.0.0 (2018-04-12) 85 | 86 | - Initial release. forked from `fast-deep-equal@1.1.0` 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for contributing! 4 | 5 | ## Before you contribute 6 | 7 | This package is a fork of [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal). This library has added handling for React. 8 | Before contributing, _please make sure the issue relates directly to this library and not fast-deep-equal_. 9 | 10 | We encourage pull requests concerning: 11 | 12 | - React features not handled in this library 13 | - Integrating updates from `fast-deep-equal` - This, unfortunately, now requires more manual work. Use the comment blocks in `index.js` 14 | to figure out what to paste and where. 15 | - Integrating tests from `fast-deep-equal` - This usually entails upgrading the `git`-based dependencies of `fast-deep-equal-git` and 16 | `npm`-published package of `fast-deep-equal` in `package.json:devDependencies`. 17 | - Bugs in this library 18 | - New tests for React 19 | - Documentation 20 | 21 | Pull requests that should be for [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal): 22 | 23 | - Equality of non-react comparisons 24 | - Performance of non-react comparisons 25 | - Tests for non-react comparisons 26 | 27 | ## Development 28 | 29 | Install the project using `yarn` (which we've standardized on for development): 30 | 31 | ```sh 32 | $ yarn install 33 | ``` 34 | 35 | **TL; DR:** - Everything you normally need to run is aggregated into: 36 | 37 | ```sh 38 | $ yarn run test 39 | $ yarn run benchmark 40 | ``` 41 | 42 | (We use `builder` to parallelize things, so tasks may output in different 43 | orders) 44 | 45 | ### Testing 46 | 47 | We write one set of tests located in: 48 | 49 | - `tests/node/**.spec.js` 50 | 51 | that run in two very different scenarios: 52 | 53 | #### Node 54 | 55 | The tests are natively run in `node` (hence why they are located in `tests/node` 56 | to begin with) without any transpilation or "build". You can run them with: 57 | 58 | ```sh 59 | # Single run 60 | $ yarn run test-node 61 | 62 | # Persistent watch 63 | $ yarn run test-node --watch 64 | ``` 65 | 66 | #### Browsers 67 | 68 | The same tests are then imported and built with `webpack` to a test bundle that 69 | can be run in arbitrary browsers. So far in CI, we execute the tests in headless 70 | Chrome on Linux in Travis and IE11 in Appveyor. 71 | 72 | To run the browser tests on your machine (note: you must already have the 73 | browser you're running installed): 74 | 75 | ```sh 76 | # Default: headless chrome 77 | $ yarn run test-browser 78 | # Example: real Chrome + Firefox + Safari 79 | $ yarn run test-browser --browsers Chrome,Firefox,Safari 80 | 81 | # IE11 (on Windows) 82 | $ yarn run test-browser-ie 83 | ``` 84 | 85 | ### Types 86 | 87 | We validate our TypeScript `index.d.ts` with two steps: 88 | 89 | ```sh 90 | # Runs the TypeScript compiler over our types 91 | $ yarn run test-ts-defs 92 | 93 | # Runs our types through a sample TypeScript file 94 | $ yarn run test-ts-usage 95 | ``` 96 | 97 | ### Style 98 | 99 | ```sh 100 | $ yarn run eslint 101 | ``` 102 | 103 | ### Size 104 | 105 | You can check how we do with minification + compression with: 106 | 107 | ```sh 108 | # Show minified output 109 | $ yarn -s compress 110 | 111 | # Display minified + gzip'ed size in bytes. 112 | $ yarn size-min-gz 113 | ``` 114 | 115 | **Note**: If the min+gz size increases, please note it in the README. If it is a significant increase, 116 | please flag to your reviewers and have a discussion about whether or not the size addition is justified. 117 | 118 | ## Before submitting a PR... 119 | 120 | ... please make sure that you have done the following: 121 | 122 | 1. Confirm that all checks are passing: 123 | 124 | ```sh 125 | $ yarn run test 126 | $ yarn run benchmark 127 | ``` 128 | 129 | 2. Confirm we don't have any significant performance regressions (check out `master` for a baseline comparison on _your_ machine). 130 | 131 | 3. Confirm you aren't impacting our bundle size. 132 | If you _do_ affect the bundle size, please update the bundle badge in the Readme by 133 | - Following the steps outlined in [size](#size): 134 | `yarn -s compress && yarn size-min-gz` 135 | - Grabbing that output and replacing the current size in the bundle_img: (`https://img.shields.io/badge/minzipped%20size-%20B-flatgreen.svg`) 136 | For example, if the new size is `650`, the new bundle_img will be `https://img.shields.io/badge/minzipped%20size-650%20B-flatgreen.svg` 137 | - _Org members:_ Update the README's benchmark comparison png using this [internal Google Sheet template](https://docs.google.com/spreadsheets/d/1GuqpO0wgPjQ9usx6sR3t0Y_HTmAdRqjXkSjs3SBsmTc/edit?usp=sharing_eip&ts=5ed1642f). 138 | 139 | ### Using changesets 140 | 141 | Our official release path is to use automation to perform the actual publishing of our packages. The steps are to: 142 | 143 | 1. A human developer adds a changeset. Ideally this is as a part of a PR that will have a version impact on a package. 144 | 2. On merge of a PR our automation system opens a "Version Packages" PR. 145 | 3. On merging the "Version Packages" PR, the automation system publishes the packages. 146 | 147 | Here are more details: 148 | 149 | ### Add a changeset 150 | 151 | When you would like to add a changeset (which creates a file indicating the type of change), in your branch/PR issue this command: 152 | 153 | ```sh 154 | $ yarn changeset 155 | ``` 156 | 157 | to produce an interactive menu. Navigate the packages with arrow keys and hit `` to select 1+ packages. Hit `` when done. Select semver versions for packages and add appropriate messages. From there, you'll be prompted to enter a summary of the change. Some tips for this summary: 158 | 159 | 1. Aim for a single line, 1+ sentences as appropriate. 160 | 2. Include issue links in GH format (e.g. `#123`). 161 | 3. You don't need to reference the current pull request or whatnot, as that will be added later automatically. 162 | 163 | After this, you'll see a new uncommitted file in `.changesets` like: 164 | 165 | ```sh 166 | $ git status 167 | # .... 168 | Untracked files: 169 | (use "git add ..." to include in what will be committed) 170 | .changeset/flimsy-pandas-marry.md 171 | ``` 172 | 173 | Review the file, make any necessary adjustments, and commit it to source. When we eventually do a package release, the changeset notes and version will be incorporated! 174 | 175 | ### Creating versions 176 | 177 | On a merge of a feature PR, the changesets GitHub action will open a new PR titled `"Version Packages"`. This PR is automatically kept up to date with additional PRs with changesets. So, if you're not ready to publish yet, just keep merging feature PRs and then merge the version packages PR later. 178 | 179 | ### Publishing packages 180 | 181 | On the merge of a version packages PR, the changesets GitHub action will publish the packages to npm. 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Formidable Labs 4 | Copyright (c) 2017 Evgeny Poberezkin 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![React Fast Compare — Formidable, We build the modern web](https://raw.githubusercontent.com/FormidableLabs/react-fast-compare/master/react-fast-compare-Hero.png)](https://formidable.com/open-source/) 2 | 3 | [![Downloads][downloads_img]][npm_site] 4 | [![Bundle Size][bundle_img]](#bundle-size) 5 | [![GH Actions Status][actions_img]][actions_site] 6 | [![Coverage Status][cov_img]][cov_site] 7 | [![npm version][npm_img]][npm_site] 8 | [![Maintenance Status][maintenance_img]](#maintenance-status) 9 | 10 | The fastest deep equal comparison for React. Very quick general-purpose deep 11 | comparison, too. Great for `React.memo` and `shouldComponentUpdate`. 12 | 13 | This is a fork of the brilliant 14 | [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) with some 15 | extra handling for React. 16 | 17 | ![benchmark chart](https://raw.githubusercontent.com/FormidableLabs/react-fast-compare/master/assets/benchmarking.png "benchmarking chart") 18 | 19 | (Check out the [benchmarking details](#benchmarking-this-library).) 20 | 21 | ## Install 22 | 23 | ```sh 24 | $ yarn add react-fast-compare 25 | # or 26 | $ npm install react-fast-compare 27 | ``` 28 | 29 | ## Highlights 30 | 31 | - ES5 compatible; works in node.js (0.10+) and browsers (IE9+) 32 | - deeply compares any value (besides objects with circular references) 33 | - handles React-specific circular references, like elements 34 | - checks equality Date and RegExp objects 35 | - should be as fast as [fast-deep-equal](https://github.com/epoberezkin/fast-deep-equal) via a single unified library, and with added guardrails for circular references. 36 | - small: under 660 bytes minified+gzipped 37 | 38 | ## Usage 39 | 40 | ```jsx 41 | const isEqual = require("react-fast-compare"); 42 | 43 | // general usage 44 | console.log(isEqual({ foo: "bar" }, { foo: "bar" })); // true 45 | 46 | // React.memo 47 | // only re-render ExpensiveComponent when the props have deeply changed 48 | const DeepMemoComponent = React.memo(ExpensiveComponent, isEqual); 49 | 50 | // React.Component shouldComponentUpdate 51 | // only re-render AnotherExpensiveComponent when the props have deeply changed 52 | class AnotherExpensiveComponent extends React.Component { 53 | shouldComponentUpdate(nextProps) { 54 | return !isEqual(this.props, nextProps); 55 | } 56 | render() { 57 | // ... 58 | } 59 | } 60 | ``` 61 | 62 | ## Do I Need `React.memo` (or `shouldComponentUpdate`)? 63 | 64 | > What's faster than a really fast deep comparison? No deep comparison at all. 65 | 66 | —This Readme 67 | 68 | Deep checks in `React.memo` or a `shouldComponentUpdate` should not be used blindly. 69 | First, see if the default 70 | [React.memo](https://reactjs.org/docs/react-api.html#reactmemo) or 71 | [PureComponent](https://reactjs.org/docs/react-api.html#reactpurecomponent) 72 | will work for you. If it won't (if you need deep checks), it's wise to make 73 | sure you've correctly indentified the bottleneck in your application by 74 | [profiling the performance](https://reactjs.org/docs/optimizing-performance.html#profiling-components-with-the-chrome-performance-tab). 75 | After you've determined that you _do_ need deep equality checks and you've 76 | identified the minimum number of places to apply them, then this library may 77 | be for you! 78 | 79 | ## Benchmarking this Library 80 | 81 | The absolute values are much less important than the relative differences 82 | between packages. 83 | 84 | Benchmarking source can be found 85 | [here](https://github.com/FormidableLabs/react-fast-compare/blob/master/benchmark/index.js). 86 | Each "operation" consists of running all relevant tests. The React benchmark 87 | uses both the generic tests and the react tests; these runs will be slower 88 | simply because there are more tests in each operation. 89 | 90 | The results below are from a local test on a laptop _(stats last updated 6/2/2020)_: 91 | 92 | ### Generic Data 93 | 94 | ``` 95 | react-fast-compare x 177,600 ops/sec ±1.73% (92 runs sampled) 96 | fast-deep-equal x 184,211 ops/sec ±0.65% (87 runs sampled) 97 | lodash.isEqual x 39,826 ops/sec ±1.32% (86 runs sampled) 98 | nano-equal x 176,023 ops/sec ±0.89% (92 runs sampled) 99 | shallow-equal-fuzzy x 146,355 ops/sec ±0.64% (89 runs sampled) 100 | fastest: fast-deep-equal 101 | ``` 102 | 103 | `react-fast-compare` and `fast-deep-equal` should be the same speed for these 104 | tests; any difference is just noise. `react-fast-compare` won't be faster than 105 | `fast-deep-equal`, because it's based on it. 106 | 107 | ### React and Generic Data 108 | 109 | ``` 110 | react-fast-compare x 86,392 ops/sec ±0.70% (93 runs sampled) 111 | fast-deep-equal x 85,567 ops/sec ±0.95% (92 runs sampled) 112 | lodash.isEqual x 7,369 ops/sec ±1.78% (84 runs sampled) 113 | fastest: react-fast-compare,fast-deep-equal 114 | ``` 115 | 116 | Two of these packages cannot handle comparing React elements, because they 117 | contain circular reference: `nano-equal` and `shallow-equal-fuzzy`. 118 | 119 | ### Running Benchmarks 120 | 121 | ```sh 122 | $ yarn install 123 | $ yarn run benchmark 124 | ``` 125 | 126 | ## Differences between this library and `fast-deep-equal` 127 | 128 | `react-fast-compare` is based on `fast-deep-equal`, with some additions: 129 | 130 | - `react-fast-compare` has `try`/`catch` guardrails for stack overflows from undetected (non-React) circular references. 131 | - `react-fast-compare` has a _single_ unified entry point for all uses. No matter what your target application is, `import equal from 'react-fast-compare'` just works. `fast-deep-equal` has multiple entry points for different use cases. 132 | 133 | This version of `react-fast-compare` tracks `fast-deep-equal@3.1.1`. 134 | 135 | ## Bundle Size 136 | 137 | There are a variety of ways to calculate bundle size for JavaScript code. 138 | You can see our size test code in the `compress` script in 139 | [`package.json`](https://github.com/FormidableLabs/react-fast-compare/blob/master/package.json). 140 | [Bundlephobia's calculation](https://bundlephobia.com/result?p=react-fast-compare) is slightly higher, 141 | as they [do not mangle during minification](https://github.com/pastelsky/package-build-stats/blob/v6.1.1/src/getDependencySizeTree.js#L139). 142 | 143 | ## License 144 | 145 | [MIT](https://github.com/FormidableLabs/react-fast-compare/blob/readme/LICENSE) 146 | 147 | ## Contributing 148 | 149 | Please see our [contributions guide](./CONTRIBUTING.md). 150 | 151 | ## Maintenance Status 152 | 153 | **Active:** Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome. 154 | 155 | [actions_img]: https://github.com/FormidableLabs/react-fast-compare/actions/workflows/ci.yml/badge.svg 156 | [actions_site]: https://github.com/formidablelabs/react-fast-compare/actions/workflows/ci.yml 157 | [cov_img]: https://codecov.io/gh/FormidableLabs/react-fast-compare/branch/master/graph/badge.svg 158 | [cov_site]: https://codecov.io/gh/FormidableLabs/react-fast-compare 159 | [npm_img]: https://badge.fury.io/js/react-fast-compare.svg 160 | [npm_site]: http://badge.fury.io/js/react-fast-compare 161 | [appveyor_img]: https://ci.appveyor.com/api/projects/status/github/formidablelabs/react-fast-compare?branch=master&svg=true 162 | [appveyor_site]: https://ci.appveyor.com/project/FormidableLabs/react-fast-compare 163 | [bundle_img]: https://img.shields.io/badge/minzipped%20size-656%20B-flatgreen.svg 164 | [downloads_img]: https://img.shields.io/npm/dm/react-fast-compare.svg 165 | [maintenance_img]: https://img.shields.io/badge/maintenance-active-flatgreen.svg 166 | -------------------------------------------------------------------------------- /assets/benchmarking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-fast-compare/6f7d8afe02e4480c32f5af16f571367cccd47abc/assets/benchmarking.png -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tests = require('../test/node/tests'); 4 | const Benchmark = require('benchmark'); 5 | 6 | const correctnessTests = []; 7 | const genericSuite = new Benchmark.Suite; 8 | const advancedSuite = new Benchmark.Suite; 9 | 10 | const equalPackages = { 11 | 'react-fast-compare': require('../index'), 12 | 'fast-deep-equal': require('fast-deep-equal/es6/react'), 13 | 'lodash.isEqual': require('lodash').isEqual, 14 | 'nano-equal': require('nano-equal'), 15 | 'shallow-equal-fuzzy': require('shallow-equal-fuzzy') 16 | }; 17 | 18 | const advancedPkgs = new Set([ 19 | 'react-fast-compare', 20 | 'fast-deep-equal', 21 | 'lodash.isEqual' 22 | ]); 23 | 24 | for (const equalName in equalPackages) { 25 | const equalFunc = equalPackages[equalName]; 26 | 27 | genericSuite.add(equalName, function() { 28 | for (const testSuite of tests.generic) { 29 | for (const test of testSuite.tests) { 30 | try { 31 | equalFunc(test.value1, test.value2); 32 | } catch (error) { 33 | // swallow errors during benchmarking. they are reported in the test section 34 | } 35 | } 36 | } 37 | }); 38 | 39 | if (advancedPkgs.has(equalName)) { 40 | advancedSuite.add(equalName, function() { 41 | for (const testSuite of tests.all) { 42 | for (const test of testSuite.tests) { 43 | try { 44 | equalFunc(test.value1, test.value2); 45 | } catch (error) { 46 | // swallow errors during benchmarking. they are reported in the test section 47 | } 48 | } 49 | } 50 | }); 51 | 52 | correctnessTests.push(() => console.log(equalName)); 53 | for (const testSuite of tests.all) { 54 | for (const test of testSuite.tests) { 55 | correctnessTests.push(() => { 56 | try { 57 | if (equalFunc(test.value1, test.value2) !== test.equal) 58 | console.error('- different result:', equalName, testSuite.description, test.description); 59 | } catch(error) { 60 | console.error('- error:', testSuite.description, test.description, error.message); 61 | } 62 | }); 63 | } 64 | } 65 | } 66 | } 67 | 68 | const chartData = {}; 69 | 70 | console.log('\n--- speed tests: generic usage ---\n'); 71 | 72 | genericSuite 73 | .on('cycle', (event) => console.log(String(event.target))) 74 | .on('complete', function () { 75 | console.log(' fastest: ' + this.filter('fastest').map('name')); 76 | chartData.categories = this.map(test => test.name); 77 | chartData.genericTestData = this.map(test => ({ 78 | x: test.name, 79 | y: test.hz, 80 | })); 81 | }) 82 | .run({async: false}); 83 | 84 | console.log('\n--- speed tests: generic and react ---\n'); 85 | 86 | advancedSuite 87 | .on('cycle', (event) => console.log(String(event.target))) 88 | .on('complete', function () { 89 | console.log(' fastest: ' + this.filter('fastest').map('name')); 90 | chartData.reactAndGenericTestData = this.map(test => ({ 91 | x: test.name, 92 | y: test.hz, 93 | })); 94 | }) 95 | .run({async: false}); 96 | 97 | // **Note**: `lodash.isEqual` gets different results for Sets, Maps 98 | // because it **is** correct and `fast-deep-equal` is not. 99 | // See: https://github.com/FormidableLabs/react-fast-compare/issues/50 100 | console.log('\n--- correctness tests: generic and react ---\n'); 101 | 102 | correctnessTests.forEach(test => test()); 103 | 104 | console.log(JSON.stringify(chartData, null, 2)); 105 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare function isEqual(a: any, b: B): a is B; 2 | export = isEqual; 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global Map:readonly, Set:readonly, ArrayBuffer:readonly */ 2 | 3 | var hasElementType = typeof Element !== 'undefined'; 4 | var hasMap = typeof Map === 'function'; 5 | var hasSet = typeof Set === 'function'; 6 | var hasArrayBuffer = typeof ArrayBuffer === 'function' && !!ArrayBuffer.isView; 7 | 8 | // Note: We **don't** need `envHasBigInt64Array` in fde es6/index.js 9 | 10 | function equal(a, b) { 11 | // START: fast-deep-equal es6/index.js 3.1.3 12 | if (a === b) return true; 13 | 14 | if (a && b && typeof a == 'object' && typeof b == 'object') { 15 | if (a.constructor !== b.constructor) return false; 16 | 17 | var length, i, keys; 18 | if (Array.isArray(a)) { 19 | length = a.length; 20 | if (length != b.length) return false; 21 | for (i = length; i-- !== 0;) 22 | if (!equal(a[i], b[i])) return false; 23 | return true; 24 | } 25 | 26 | // START: Modifications: 27 | // 1. Extra `has &&` helpers in initial condition allow es6 code 28 | // to co-exist with es5. 29 | // 2. Replace `for of` with es5 compliant iteration using `for`. 30 | // Basically, take: 31 | // 32 | // ```js 33 | // for (i of a.entries()) 34 | // if (!b.has(i[0])) return false; 35 | // ``` 36 | // 37 | // ... and convert to: 38 | // 39 | // ```js 40 | // it = a.entries(); 41 | // while (!(i = it.next()).done) 42 | // if (!b.has(i.value[0])) return false; 43 | // ``` 44 | // 45 | // **Note**: `i` access switches to `i.value`. 46 | var it; 47 | if (hasMap && (a instanceof Map) && (b instanceof Map)) { 48 | if (a.size !== b.size) return false; 49 | it = a.entries(); 50 | while (!(i = it.next()).done) 51 | if (!b.has(i.value[0])) return false; 52 | it = a.entries(); 53 | while (!(i = it.next()).done) 54 | if (!equal(i.value[1], b.get(i.value[0]))) return false; 55 | return true; 56 | } 57 | 58 | if (hasSet && (a instanceof Set) && (b instanceof Set)) { 59 | if (a.size !== b.size) return false; 60 | it = a.entries(); 61 | while (!(i = it.next()).done) 62 | if (!b.has(i.value[0])) return false; 63 | return true; 64 | } 65 | // END: Modifications 66 | 67 | if (hasArrayBuffer && ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { 68 | length = a.length; 69 | if (length != b.length) return false; 70 | for (i = length; i-- !== 0;) 71 | if (a[i] !== b[i]) return false; 72 | return true; 73 | } 74 | 75 | if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; 76 | // START: Modifications: 77 | // Apply guards for `Object.create(null)` handling. See: 78 | // - https://github.com/FormidableLabs/react-fast-compare/issues/64 79 | // - https://github.com/epoberezkin/fast-deep-equal/issues/49 80 | if (a.valueOf !== Object.prototype.valueOf && typeof a.valueOf === 'function' && typeof b.valueOf === 'function') return a.valueOf() === b.valueOf(); 81 | if (a.toString !== Object.prototype.toString && typeof a.toString === 'function' && typeof b.toString === 'function') return a.toString() === b.toString(); 82 | // END: Modifications 83 | 84 | keys = Object.keys(a); 85 | length = keys.length; 86 | if (length !== Object.keys(b).length) return false; 87 | 88 | for (i = length; i-- !== 0;) 89 | if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; 90 | // END: fast-deep-equal 91 | 92 | // START: react-fast-compare 93 | // custom handling for DOM elements 94 | if (hasElementType && a instanceof Element) return false; 95 | 96 | // custom handling for React/Preact 97 | for (i = length; i-- !== 0;) { 98 | if ((keys[i] === '_owner' || keys[i] === '__v' || keys[i] === '__o') && a.$$typeof) { 99 | // React-specific: avoid traversing React elements' _owner 100 | // Preact-specific: avoid traversing Preact elements' __v and __o 101 | // __v = $_original / $_vnode 102 | // __o = $_owner 103 | // These properties contain circular references and are not needed when 104 | // comparing the actual elements (and not their owners) 105 | // .$$typeof and ._store on just reasonable markers of elements 106 | 107 | continue; 108 | } 109 | 110 | // all other properties should be traversed as usual 111 | if (!equal(a[keys[i]], b[keys[i]])) return false; 112 | } 113 | // END: react-fast-compare 114 | 115 | // START: fast-deep-equal 116 | return true; 117 | } 118 | 119 | return a !== a && b !== b; 120 | } 121 | // end fast-deep-equal 122 | 123 | module.exports = function isEqual(a, b) { 124 | try { 125 | return equal(a, b); 126 | } catch (error) { 127 | if (((error.message || '').match(/stack|recursion/i))) { 128 | // warn on circular references, don't crash 129 | // browsers give this different errors name and messages: 130 | // chrome/safari: "RangeError", "Maximum call stack size exceeded" 131 | // firefox: "InternalError", too much recursion" 132 | // edge: "Error", "Out of stack space" 133 | console.warn('react-fast-compare cannot handle circular refs'); 134 | return false; 135 | } 136 | // some other error. we should definitely know about these 137 | throw error; 138 | } 139 | }; 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-fast-compare", 3 | "version": "3.2.2", 4 | "description": "Fastest deep equal comparison for React. Great for React.memo & shouldComponentUpdate. Also really fast general-purpose deep comparison.", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "scripts": { 8 | "preversion": "yarn test", 9 | "benchmark": "node benchmark", 10 | "eslint": "eslint \"*.js\" benchmark test", 11 | "tslint": "eslint test/typescript/*.tsx", 12 | "test-browser": "karma start test/browser/karma.conf.js", 13 | "test-node": "mocha \"test/node/*.spec.js\"", 14 | "test-node-cov": "nyc mocha \"test/node/*.spec.js\"", 15 | "test-ts-usage": "tsc --esModuleInterop --jsx react --noEmit test/typescript/sample-react-redux-usage.tsx test/typescript/sample-usage.tsx", 16 | "test-ts-defs": "tsc --target ES5 index.d.ts", 17 | "test": "builder concurrent --buffer eslint tslint test-ts-usage test-ts-defs test-node-cov test-browser", 18 | "compress": "terser --compress --mangle=\"toplevel:true\" -- index.js", 19 | "size-min-gz": "yarn -s compress | gzip -9 | wc -c", 20 | "changeset": "changeset" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/FormidableLabs/react-fast-compare" 25 | }, 26 | "keywords": [ 27 | "fast", 28 | "equal", 29 | "react", 30 | "compare", 31 | "shouldComponentUpdate", 32 | "deep-equal" 33 | ], 34 | "author": "Chris Bolin", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/FormidableLabs/react-fast-compare/issues" 38 | }, 39 | "homepage": "https://github.com/FormidableLabs/react-fast-compare", 40 | "devDependencies": { 41 | "@babel/core": "^7.21.0", 42 | "@babel/preset-env": "^7.20.2", 43 | "@changesets/cli": "^2.26.1", 44 | "@svitejs/changesets-changelog-github-compact": "^0.1.1", 45 | "@testing-library/dom": "^9.0.1", 46 | "@testing-library/preact": "^3.2.3", 47 | "@types/node": "^18.15.0", 48 | "@types/react": "^16.9.35", 49 | "@types/react-dom": "^16.9.8", 50 | "@types/react-redux": "^7.1.25", 51 | "@typescript-eslint/parser": "^5.54.1", 52 | "assert": "^2.0.0", 53 | "babel-loader": "^9.1.2", 54 | "benchmark": "^2.1.4", 55 | "builder": "^5.0.0", 56 | "codecov": "^3.8.3", 57 | "core-js": "^3.29.0", 58 | "eslint": "^8.35.0", 59 | "eslint-plugin-react": "^7.32.2", 60 | "fast-deep-equal": "3.1.3", 61 | "fast-deep-equal-git": "epoberezkin/fast-deep-equal#v3.1.3", 62 | "jsdom": "^21.1.0", 63 | "jsdom-global": "^3.0.2", 64 | "karma": "^6.4.1", 65 | "karma-chrome-launcher": "^3.1.1", 66 | "karma-firefox-launcher": "^2.1.2", 67 | "karma-mocha": "^2.0.1", 68 | "karma-mocha-reporter": "^2.2.5", 69 | "karma-safari-launcher": "^1.0.0", 70 | "karma-webpack": "^5.0.0", 71 | "lodash": "^4.17.10", 72 | "mocha": "^10.2.0", 73 | "nano-equal": "^2.0.2", 74 | "nyc": "^15.1.0", 75 | "preact": "^10.13.1", 76 | "process": "^0.11.10", 77 | "react": "^18.2.0", 78 | "react-dom": "^18.2.0", 79 | "react-redux": "^8.0.5", 80 | "react-test-renderer": "^18.2.0", 81 | "redux": "^4.2.1", 82 | "shallow-equal-fuzzy": "0.0.2", 83 | "sinon": "^15.0.1", 84 | "terser": "^5.16.6", 85 | "typescript": "^4.9.5", 86 | "webpack": "^5.76.0" 87 | }, 88 | "publishConfig": { 89 | "provenance": true 90 | }, 91 | "nyc": { 92 | "exclude": [ 93 | "**/test/**", 94 | "node_modules" 95 | ], 96 | "reporter": [ 97 | "lcov", 98 | "text-summary" 99 | ] 100 | }, 101 | "files": [ 102 | "index.js", 103 | "index.d.ts" 104 | ], 105 | "types": "index.d.ts" 106 | } -------------------------------------------------------------------------------- /react-fast-compare-Hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FormidableLabs/react-fast-compare/6f7d8afe02e4480c32f5af16f571367cccd47abc/react-fast-compare-Hero.png -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | extends: [ 'eslint:recommended', 'plugin:react/recommended' ] 4 | parser: '@typescript-eslint/parser' 5 | parserOptions: 6 | sourceType: module 7 | settings: 8 | react: 9 | version: detect 10 | globals: 11 | document: false 12 | Element: false 13 | rules: 14 | no-var: 2 15 | prefer-const: 2 16 | -------------------------------------------------------------------------------- /test/browser/browser.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | 6 | const equal = require('../..'); 7 | 8 | const element1 = document.createElement('div'); 9 | const element2 = document.createElement('div'); 10 | const element3 = document.createElement('input'); 11 | 12 | const suites = [{ 13 | description: 'DOM elements', 14 | tests: [ 15 | { 16 | description: 'equal DOM elements', 17 | value1: element1, 18 | value2: element1, 19 | equal: true 20 | }, 21 | { 22 | description: 'comparison of different elements', 23 | value1: element1, 24 | value2: element2, 25 | equal: false 26 | }, 27 | { 28 | description: 'comparison of elements with different types', 29 | value1: element1, 30 | value2: element3, 31 | equal: false 32 | }, 33 | ] 34 | }]; 35 | 36 | describe('browser', function () { 37 | let sandbox; 38 | 39 | beforeEach(() => { 40 | sandbox = sinon.createSandbox(); 41 | sandbox.stub(console, 'warn'); 42 | }); 43 | 44 | afterEach(() => { 45 | sandbox.restore(); 46 | }); 47 | 48 | suites.forEach(function (suite) { 49 | describe(suite.description, function () { 50 | suite.tests.forEach(function (test) { 51 | (test.skip ? it.skip : it)(test.description, function () { 52 | assert.strictEqual(equal(test.value1, test.value2), test.equal); 53 | }); 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/browser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Polyfills for IE in React 16 and other dependencies. 4 | require('core-js/features/array/from'); 5 | require('core-js/features/object/entries'); 6 | require('core-js/features/promise'); 7 | require('core-js/features/map'); 8 | require('core-js/features/set'); 9 | require('core-js/features/weak-map'); 10 | require('core-js/features/regexp/flags'); 11 | require('core-js/features/symbol'); 12 | 13 | // Re-use node tests. 14 | const testsContext = require.context('..', true, /\.spec\.js$/); 15 | testsContext.keys().forEach(testsContext); 16 | -------------------------------------------------------------------------------- /test/browser/karma.conf.ie.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(config) { 4 | require('./karma.conf.js')(config); 5 | config.set({ 6 | plugins: config.plugins.concat('karma-ie-launcher'), 7 | browsers: ['IE'] 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /test/browser/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | // **Debugging Help** 7 | // We normally dislike commented out code, but as Karma doesn't easily produce 8 | // a bundle, debugging bundle errors is a pain, particularly on ie11. 9 | // To help with this, uncomment this plugin to write out all Karma webpack 10 | // assets like `test/browser/index.js` to disk as `~/Desktops/test-browser-index.js` 11 | // and then inspect the failing lines you need. 12 | // 13 | // Add the plugin into config for `webpack` below with: 14 | // ``` 15 | // plugins: [ 16 | // new WriteAssetPlugin() 17 | // ] 18 | // ``` 19 | // 20 | // const fs = require('fs').promises; 21 | // const os = require('os'); 22 | // 23 | // class WriteAssetPlugin { 24 | // apply(compiler) { 25 | // compiler.hooks.emit.tapPromise("WriteAssetPlugin", this.writeAsset.bind(this)); 26 | // } 27 | // 28 | // async writeAsset(compiler) { 29 | // const { assets } = compiler; 30 | // 31 | // await Promise.all(Object.entries(assets).map(async ([file, src]) => { 32 | // const outFile = path.join(os.homedir(), "Desktop", file.split(path.sep).join("-")); 33 | // await fs.writeFile(outFile, src.source()); 34 | // })); 35 | // } 36 | // } 37 | 38 | module.exports = function(config) { 39 | config.set({ 40 | basePath: '../..', 41 | frameworks: ['mocha'], 42 | files: [ 43 | 'test/browser/index.js' 44 | ], 45 | preprocessors: { 46 | 'test/browser/index.js': ['webpack'] 47 | }, 48 | webpack: { 49 | mode: 'development', 50 | // Normally, would follow this guide for source maps: 51 | // https://github.com/webpack-contrib/karma-webpack#source-maps 52 | // Unfortunately, karma-webpack doesn't work with source maps w/ babel. 53 | // https://github.com/webpack-contrib/karma-webpack/issues/176 54 | devtool: false, 55 | module: { 56 | rules: [ 57 | { 58 | test: /\.js$/, 59 | enforce: 'pre', 60 | include: [ 61 | path.resolve(__dirname, '..'), 62 | // Transpile the `fast-deep-equal` tests for all browsers. 63 | // (The node tests work off real code with ES.next stuff). 64 | path.join( 65 | path.dirname(require.resolve('fast-deep-equal-git/package.json')), 66 | 'spec' 67 | ) 68 | ], 69 | loader: 'babel-loader', 70 | options: { 71 | presets: ['@babel/preset-env'] 72 | } 73 | } 74 | ] 75 | }, 76 | plugins: [ 77 | new webpack.ProvidePlugin({ 78 | assert: 'assert', 79 | process: 'process' 80 | }) 81 | ] 82 | }, 83 | exclude: [], 84 | port: 8080, 85 | logLevel: config.LOG_INFO, 86 | colors: true, 87 | autoWatch: false, 88 | // Run a customized instance of headless chrome for dev + Travis CI. 89 | browsers: ['ChromeHeadlessCustom'], 90 | customLaunchers: { 91 | ChromeHeadlessCustom: { 92 | base: 'ChromeHeadless', 93 | // --no-sandbox for https://github.com/travis-ci/docs-travis-ci-com/pull/1671/files 94 | flags: ['--no-sandbox'] 95 | } 96 | }, 97 | reporters: ['mocha'/* TODO, 'coverage'*/], 98 | browserNoActivityTimeout: 60000, 99 | plugins: [ 100 | 'karma-chrome-launcher', 101 | 'karma-firefox-launcher', 102 | 'karma-safari-launcher', 103 | //'karma-coverage', 104 | 'karma-mocha', 105 | 'karma-mocha-reporter', 106 | 'karma-webpack' 107 | ], 108 | coverageReporter: { 109 | type: 'text' 110 | }, 111 | browserConsoleLogOptions: { 112 | level: 'log', 113 | format: '%b %T: %m', 114 | terminal: true 115 | }, 116 | captureTimeout: 100000, 117 | singleRun: true 118 | }); 119 | }; -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | -------------------------------------------------------------------------------- /test/node/advanced.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable react/prop-types */ 4 | /* eslint-disable react/no-children-prop */ 5 | 6 | const assert = require('assert'); 7 | const jsdom = require('jsdom-global'); 8 | const sinon = require('sinon'); 9 | 10 | const React = require('react'); 11 | const ReactTestRenderer = require('react-test-renderer'); 12 | 13 | const Preact = require('preact/compat'); 14 | const PreactTestRenderer = require('@testing-library/preact'); 15 | 16 | const equal = require('../..'); 17 | const tests = require('./tests'); 18 | 19 | // `jsdom-global` does a lazy require under the hood to `jsdom` which is 20 | // super expensive. We deliberately call + cleanup to take the require hit 21 | // early and seed the require cache so subsequent calls are fast. 22 | jsdom()(); 23 | 24 | class ReactChild extends React.Component { 25 | shouldComponentUpdate(nextProps) { 26 | // this.props.children is a h1 with a circular reference to its owner, Container 27 | return !equal(this.props, nextProps); 28 | } 29 | render() { 30 | return null; 31 | } 32 | } 33 | 34 | class ReactContainer extends React.Component { 35 | render() { 36 | return React.createElement(ReactChild, { 37 | children: [ 38 | React.createElement('h1', this.props.title || ''), 39 | React.createElement('h2', this.props.subtitle || ''), 40 | ], 41 | }); 42 | } 43 | } 44 | 45 | class PreactChild extends Preact.Component { 46 | shouldComponentUpdate(nextProps) { 47 | // this.props.children is a h1 with a circular reference to its owner, Container 48 | return !equal(this.props, nextProps); 49 | } 50 | render() { 51 | return null; 52 | } 53 | } 54 | 55 | class PreactContainer extends Preact.Component { 56 | render() { 57 | return Preact.createElement(PreactChild, { 58 | children: [ 59 | Preact.createElement('h1', this.props.title || ''), 60 | Preact.createElement('h2', this.props.subtitle || ''), 61 | ], 62 | }); 63 | } 64 | } 65 | 66 | describe('advanced', () => { 67 | let sandbox; 68 | let warnStub; 69 | 70 | beforeEach(() => { 71 | sandbox = sinon.createSandbox(); 72 | warnStub = sandbox.stub(console, 'warn'); 73 | }); 74 | 75 | afterEach(() => { 76 | sandbox.restore(); 77 | }); 78 | 79 | describe('React', () => { 80 | let reactChildRenderSpy; 81 | 82 | beforeEach(() => { 83 | reactChildRenderSpy = sandbox.spy(ReactChild.prototype, 'render'); 84 | }); 85 | 86 | describe('element (with circular references)', () => { 87 | it('compares without warning or errors', () => { 88 | const reactApp = ReactTestRenderer.create( 89 | React.createElement(ReactContainer) 90 | ); 91 | reactApp.update(React.createElement(ReactContainer)); 92 | assert.strictEqual(warnStub.callCount, 0); 93 | }); 94 | it('elements of same type and props are equal', () => { 95 | const reactApp = ReactTestRenderer.create( 96 | React.createElement(ReactContainer) 97 | ); 98 | reactApp.update(React.createElement(ReactContainer)); 99 | assert.strictEqual(reactChildRenderSpy.callCount, 1); 100 | }); 101 | it('elements of same type with different props are not equal', () => { 102 | const reactApp = ReactTestRenderer.create( 103 | React.createElement(ReactContainer) 104 | ); 105 | reactApp.update(React.createElement(ReactContainer, { title: 'New' })); 106 | assert.strictEqual(reactChildRenderSpy.callCount, 2); 107 | }); 108 | }); 109 | }); 110 | 111 | describe('Preact', () => { 112 | let cleanupJsDom; 113 | let preactChildRenderSpy; 114 | 115 | beforeEach(() => { 116 | cleanupJsDom = jsdom(); 117 | preactChildRenderSpy = sandbox.spy(PreactChild.prototype, 'render'); 118 | }); 119 | 120 | afterEach(() => { 121 | PreactTestRenderer.cleanup(); 122 | if (cleanupJsDom) cleanupJsDom(); 123 | }); 124 | 125 | describe('element (with circular references)', () => { 126 | it('compares without warning or errors', () => { 127 | const { rerender } = PreactTestRenderer.render( 128 | Preact.createElement(PreactContainer) 129 | ); 130 | rerender(Preact.createElement(PreactContainer)); 131 | assert.strictEqual(warnStub.callCount, 0); 132 | }); 133 | it('elements of same type and props are equal', () => { 134 | const { rerender } = PreactTestRenderer.render( 135 | Preact.createElement(PreactContainer) 136 | ); 137 | rerender(Preact.createElement(PreactContainer)); 138 | assert.strictEqual(preactChildRenderSpy.callCount, 1); 139 | }); 140 | it('elements of same type with different props are not equal', () => { 141 | const { rerender } = PreactTestRenderer.render( 142 | Preact.createElement(PreactContainer) 143 | ); 144 | rerender(Preact.createElement(PreactContainer, { title: 'New' })); 145 | assert.strictEqual(preactChildRenderSpy.callCount, 2); 146 | }); 147 | }); 148 | }); 149 | 150 | describe('warnings', () => { 151 | describe('circular reference', () => { 152 | it('warns on circular refs but do not throw', () => { 153 | const circularA = { a: 1 }; 154 | circularA.self = circularA; 155 | const circularB = { a: 1 }; 156 | circularB.self = circularB; 157 | equal(circularA, circularB); 158 | assert.strictEqual(warnStub.callCount, 1); 159 | }); 160 | }); 161 | describe('basics usage', () => { 162 | it('never warns', () => { 163 | tests.generic.forEach((suite) => { 164 | suite.tests.forEach((test) => { 165 | assert.strictEqual( 166 | equal(test.value1, test.value2), 167 | test.equal, 168 | test.description 169 | ); 170 | }); 171 | }); 172 | assert.strictEqual( 173 | warnStub.callCount, 174 | 0, 175 | `console.warn called ${ 176 | warnStub.callCount 177 | } with arguments: ${JSON.stringify(warnStub.args)}` 178 | ); 179 | }); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /test/node/basics.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | 6 | const equal = require('../..'); 7 | const tests = require('./tests'); 8 | 9 | describe('basics', function() { 10 | let sandbox; 11 | 12 | beforeEach(() => { 13 | sandbox = sinon.createSandbox(); 14 | sandbox.stub(console, 'warn'); 15 | }); 16 | 17 | afterEach(() => { 18 | sandbox.restore(); 19 | }); 20 | 21 | tests.all.forEach(function (suite) { 22 | describe(suite.description, function() { 23 | suite.tests.forEach(function (test) { 24 | (test.skip ? it.skip : it)(test.description, function() { 25 | assert.strictEqual(equal(test.value1, test.value2), test.equal); 26 | }); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/node/tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const generic = require('fast-deep-equal-git/spec/tests.js'); 3 | const es6 = require('fast-deep-equal-git/spec/es6tests.js'); 4 | 5 | const reactElementA = { 6 | '$$typeof': 'react.element', 7 | type: 'div', 8 | key: null, 9 | ref: null, 10 | props: { x: 1 }, 11 | _owner: {}, 12 | _store: {} 13 | }; 14 | // in reality the _owner object is much more complex (and contains over dozen circular references) 15 | reactElementA._owner.children = [reactElementA]; 16 | 17 | const reactElementA2 = { 18 | '$$typeof': 'react.element', 19 | type: 'div', 20 | key: null, 21 | ref: null, 22 | props: { x: 1 }, 23 | _owner: {}, 24 | _store: {} 25 | }; 26 | reactElementA2._owner.children = [reactElementA2]; 27 | 28 | const reactElementB = { 29 | '$$typeof': 'react.element', 30 | type: 'div', 31 | key: null, 32 | ref: null, 33 | props: { x: 2 }, 34 | _owner: {}, 35 | _store: {} 36 | }; 37 | reactElementB._owner.children = [reactElementB]; 38 | 39 | 40 | const react = [ 41 | { 42 | description: 'React elements', 43 | reactSpecific: true, 44 | tests: [ 45 | { 46 | description: 'an element compared with itself', 47 | value1: reactElementA, 48 | value2: reactElementA, 49 | equal: true 50 | }, 51 | { 52 | description: 'two elements equal by value', 53 | value1: reactElementA, 54 | value2: reactElementA2, 55 | equal: true 56 | }, 57 | { 58 | description: 'two elements unequal by value', 59 | value1: reactElementA, 60 | value2: reactElementB, 61 | equal: false 62 | } 63 | ] 64 | } 65 | ]; 66 | 67 | // Additional customized behavior. 68 | const custom = [ 69 | { 70 | description: 'Custom tests', 71 | tests: [ 72 | { 73 | description: 'Object.create(null) equal', 74 | value1: Object.create(null), 75 | value2: Object.create(null), 76 | equal: true 77 | }, 78 | { 79 | description: 'Object.create(null) unequal to empty object', 80 | value1: Object.create(null), 81 | value2: {}, 82 | equal: false 83 | }, 84 | { 85 | description: 'Object.create(null) unequal to non-empty object', 86 | value1: Object.create(null), 87 | value2: { a: 1 }, 88 | equal: false 89 | }, 90 | // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#null-prototype_objects 91 | { 92 | description: 'Object.create(null) equal to vanilla null prototype deep objects', 93 | value1: Object.assign(Object.create(null), { a: 1, b: { c: true } }), 94 | value2: { __proto__: null, a: 1, b: { c: true } }, 95 | equal: true 96 | }, 97 | // Object.create(null) has a different `constructor` than a vanilla, non-null object. 98 | { 99 | description: 'Object.create(null) unequal to vanilla deep objects', 100 | value1: Object.assign(Object.create(null), { a: 1, b: { c: true } }), 101 | value2: { a: 1, b: { c: true } }, 102 | equal: false 103 | }, 104 | { 105 | description: 'Object.create(null) equal for deep objects', 106 | value1: Object.assign(Object.create(null), { a: 1, b: { c: true } }), 107 | value2: Object.assign(Object.create(null), { b: { c: true }, a: 1 }), 108 | equal: true 109 | } 110 | ] 111 | } 112 | ]; 113 | 114 | module.exports = { 115 | generic, 116 | es6, 117 | react, 118 | custom, 119 | all: [].concat(generic, es6, react, custom), 120 | }; 121 | -------------------------------------------------------------------------------- /test/typescript/sample-react-redux-usage.tsx: -------------------------------------------------------------------------------- 1 | // This file exists to test our types against sample user code 2 | // This is compiled using `tsc` in our `test-ts-usage` script 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { Provider, useSelector } from 'react-redux'; 6 | import { createStore } from 'redux'; 7 | 8 | import equal from '../../index.js'; 9 | 10 | type IState = { 11 | items: string[]; 12 | }; 13 | 14 | type IAction = { 15 | type: string; 16 | payload: any; 17 | }; 18 | 19 | const initialState: IState = { 20 | items: [], 21 | }; 22 | 23 | const reducer = (state: IState, action: IAction) => { 24 | return state; 25 | }; 26 | 27 | const lengthSelector = (state: IState): number => state.items.length; 28 | 29 | const store = createStore(reducer, initialState); 30 | 31 | function Test() { 32 | const length = useSelector(lengthSelector, equal); 33 | return ( 34 |
35 | Testing react-redux useSelector. There are 36 | {length.toExponential()} items. 37 |
38 | ); 39 | } 40 | 41 | ReactDOM.render( 42 | 43 | 44 | , 45 | document.getElementById('root') 46 | ); 47 | -------------------------------------------------------------------------------- /test/typescript/sample-usage.tsx: -------------------------------------------------------------------------------- 1 | // This file exists to test our types against sample user code 2 | // This is compiled using `tsc` in our `test-ts-usage` script 3 | import React from 'react'; 4 | import equal from '../../index.js'; 5 | 6 | type ITodo = { 7 | text: string; 8 | id: string; 9 | }; 10 | 11 | const testArr: ITodo[] = [ 12 | { text: 'green', id: '23' }, 13 | { text: 'sunshine', id: '1' }, 14 | { text: 'mountain', id: '11' }, 15 | { text: 'air', id: '8' }, 16 | { text: 'plants', id: '9' }, 17 | ]; 18 | 19 | type IProps = { 20 | todo: ITodo; 21 | }; 22 | 23 | class TestChild extends React.Component { 24 | shouldComponentUpdate(nextProps: IProps) { 25 | return !equal(this.props, nextProps); 26 | } 27 | 28 | render() { 29 | const todo = this.props.todo; 30 | return
{todo.text}
; 31 | } 32 | } 33 | 34 | class TestContainer extends React.Component { 35 | render() { 36 | return testArr.map((item) => ); 37 | } 38 | } 39 | 40 | export default TestContainer; 41 | -------------------------------------------------------------------------------- /test/typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "jsx": "react", 5 | "target": "ES6", 6 | "allowSyntheticDefaultImports": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true 4 | }, 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "es6": true 9 | }, 10 | "references": [{ "path": "test/typescript" }], 11 | "parserOptions": { 12 | "sourceType": "module" 13 | } 14 | } 15 | --------------------------------------------------------------------------------