├── .all-contributorsrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmrc ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── other ├── CODE_OF_CONDUCT.md ├── MAINTAINING.md ├── USERS.md └── manual-releases.md ├── package.json ├── src ├── __tests__ │ └── index.js └── index.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "use-conditional-effect", 3 | "projectOwner": "alexkrolick", 4 | "files": [ 5 | "CONTRIBUTORS.md" 6 | ], 7 | "imageSize": 50, 8 | "commit": false, 9 | "contributors": [ 10 | { 11 | "login": "alexkrolick", 12 | "name": "Alex Krolick", 13 | "avatar_url": "https://avatars3.githubusercontent.com/u/1571667?v=4", 14 | "profile": "https://alexkrolick.com", 15 | "contributions": [ 16 | "maintenance" 17 | ] 18 | }, 19 | { 20 | "login": "kentcdodds", 21 | "name": "Kent C. Dodds", 22 | "avatar_url": "https://avatars0.githubusercontent.com/u/1500684?v=4", 23 | "profile": "https://kentcdodds.com", 24 | "contributions": [ 25 | "infra" 26 | ] 27 | } 28 | ], 29 | "repoType": "github", 30 | "repoHost": "https://github.com", 31 | "contributorsPerLine": 3 32 | } 33 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | - `use-conditional-effect` version: 15 | - `node` version: 16 | - `npm` (or `yarn`) version: 17 | 18 | Relevant code or config 19 | 20 | ```javascript 21 | ``` 22 | 23 | What you did: 24 | 25 | What happened: 26 | 27 | 28 | 29 | Reproduction repository: 30 | 31 | 35 | 36 | Problem description: 37 | 38 | Suggested solution: 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | **What**: 20 | 21 | 22 | 23 | **Why**: 24 | 25 | 26 | 27 | **How**: 28 | 29 | 30 | 31 | **Checklist**: 32 | 33 | 34 | 35 | 36 | - [ ] Documentation 37 | - [ ] Tests 38 | - [ ] Ready to be merged 39 | 40 | - [ ] Added myself to contributors table 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | .opt-in 5 | .opt-out 6 | .DS_Store 7 | .eslintcache 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": false, 9 | "jsxBracketSameLine": false, 10 | "proseWrap": "always" 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - ~/.npm 6 | notifications: 7 | email: false 8 | node_js: '8' 9 | install: npm install 10 | script: npm run validate 11 | after_success: kcd-scripts travis-after-success 12 | branches: 13 | only: master 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The changelog is automatically updated using 4 | [semantic-release](https://github.com/semantic-release/semantic-release). You 5 | can see it on the [releases page](../../releases). 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for being willing to contribute! 4 | 5 | **Working on your first Pull Request?** You can learn how from this _free_ 6 | series [How to Contribute to an Open Source Project on GitHub][egghead] 7 | 8 | ## Project setup 9 | 10 | 1. Fork and clone the repo 11 | 2. Run `npm run setup -s` to install dependencies and run validation 12 | 3. Create a branch for your PR with `git checkout -b pr/your-branch-name` 13 | 14 | > Tip: Keep your `master` branch pointing at the original repository and make 15 | > pull requests from branches on your fork. To do this, run: 16 | > 17 | > ``` 18 | > git remote add upstream https://github.com/alexkrolick/use-conditional-effect.git 19 | > git fetch upstream 20 | > git branch --set-upstream-to=upstream/master master 21 | > ``` 22 | > 23 | > This will add the original repository as a "remote" called "upstream," Then 24 | > fetch the git information from that remote, then set your local `master` 25 | > branch to use the upstream master branch whenever you run `git pull`. Then you 26 | > can make all of your pull request branches based on this `master` branch. 27 | > Whenever you want to update your version of `master`, do a regular `git pull`. 28 | 29 | ## Committing and Pushing changes 30 | 31 | Please make sure to run the tests before you commit your changes. You can run 32 | `npm run test:update` which will update any snapshots that need updating. Make 33 | sure to include those changes (if they exist) in your commit. 34 | 35 | ### opt into git hooks 36 | 37 | There are git hooks set up with this project that are automatically installed 38 | when you install dependencies. They're really handy, but are turned off by 39 | default (so as to not hinder new contributors). You can opt into these by 40 | creating a file called `.opt-in` at the root of the project and putting this 41 | inside: 42 | 43 | ``` 44 | pre-commit 45 | ``` 46 | 47 | ## Help needed 48 | 49 | Please checkout the [the open issues][issues] 50 | 51 | Also, please watch the repo and respond to questions/bug reports/feature 52 | requests! Thanks! 53 | 54 | [egghead]: 55 | https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github 56 | [all-contributors]: https://github.com/kentcdodds/all-contributors 57 | [issues]: https://github.com/alexkrolick/use-conditional-effect/issues 58 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | Thanks goes to these people ([emoji key][emojis]): 4 | 5 | 6 | 7 |
Alex Krolick
Alex Krolick

🚧
Kent C. Dodds
Kent C. Dodds

🚇
8 | 9 | 10 | 11 | This project follows the [all-contributors][all-contributors] specification. 12 | Contributions of any kind welcome! 13 | 14 | [emojis]: https://github.com/kentcdodds/all-contributors#emoji-key 15 | [all-contributors]: https://github.com/kentcdodds/all-contributors 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Alex Krolick 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

use-conditional-effect 🎲

3 | 4 |

5 | 6 | It's React's [useEffect][useeffect] hook, except you can pass a comparison 7 | function. 8 | 9 |

10 | 11 |
12 | 13 |
14 | 15 | [![Build Status][build-badge]][build] 16 | [![Code Coverage][coverage-badge]][coverage] 17 | [![version][version-badge]][package] [![downloads][downloads-badge]][npmtrends] 18 | [![MIT License][license-badge]][license] 19 | 20 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)][contributors] 21 | [![PRs Welcome][prs-badge]][prs] [![Code of Conduct][coc-badge]][coc] 22 | 23 | [![Watch on GitHub][github-watch-badge]][github-watch] 24 | [![Star on GitHub][github-star-badge]][github-star] 25 | [![Tweet][twitter-badge]][twitter] 26 | 27 | ## The problem 28 | 29 | React's built-in `useEffect` hook has a second argument called the "dependencies 30 | array" and it allows you to decide when React will call your effect callback. 31 | React will do a comparison between each of the values (using `Object.is`, which 32 | is similar to `===`) to determine whether your effect callback should be called. 33 | 34 | The idea behind the dependencies array is that the identity of the items in the 35 | array will tell you when to run the effect. 36 | 37 | There are several cases where object identity is not a good choice for 38 | triggering effects: 39 | 40 | - You want to call a callback function, which may change identity, _when certain 41 | things change_ (not when the function itself changes). Sometimes you can 42 | memoize the function with useCallback, or assume someone else has done that, 43 | but doing so couples your effect condition to external code. 44 | - The values you need to compare require custom comparison (like a 45 | deep/recursive equality check). 46 | 47 | Here's an example situation: 48 | 49 | ```jsx 50 | function Query({query, variables}) { 51 | // some code... 52 | 53 | // Who knows if this is a stable reference! 54 | const getQueryResult = useMyGraphQlLibrary() 55 | 56 | React.useEffect( 57 | () => { 58 | getQueryResult(query, variables) 59 | }, 60 | // ⚠️ PROBLEMS! 61 | // - variables is a new object every render but we only want 62 | // to run the effect when the username property changes 63 | // - getQueryResult might change but we don't want to run the 64 | // effect when that happens 65 | [query, variables, getQueryResult], 66 | ) 67 | 68 | return
{/* awesome UI here */}
69 | } 70 | 71 | function QueryPageThing({username}) { 72 | const query = ` 73 | query getUserData($username: String!) { 74 | user(login: $username) { 75 | name 76 | } 77 | } 78 | ` 79 | const variables = {username} 80 | // poof! Every render `variables` will be a new object! 81 | return 82 | } 83 | ``` 84 | 85 | > **Note** 86 | > 87 | > You could also solve the first problem if the `QueryPageThing` created the 88 | > variables object like this: 89 | > `const variables = React.useMemo(() => ({username}), [username])`. Then you 90 | > wouldn't need this package. But sometimes you're writing a custom hook and you 91 | > don't have control on what kinds of things people are passing you (or you want 92 | > to give them a nice ergonomic API that can handle new objects every render). 93 | > 94 | > In the second case, technically you don't have to add the callback to the 95 | > dependencies array. But the [exhaustive-deps][exhaustive-deps-lint] ESLint 96 | > rule automatically will add it unless you disable the rule. 97 | 98 | ## This solution 99 | 100 | This is a replacement for `React.useEffect` that accepts a comparison function 101 | in addition to the dependencies array. The comparison function gets the previous 102 | value of the dependencies as well as the current value, and the effect only runs 103 | if it returns true. Additionally, dependencies doesn't have to be an array, it 104 | can be an object or any other value. 105 | 106 | ## Table of Contents 107 | 108 | 109 | 110 | 111 | - [Installation](#installation) 112 | - [Usage](#usage) 113 | - [Compatibility with `React.useEffect`](#compatibility-with-reactuseeffect) 114 | - [Other Solutions](#other-solutions) 115 | - [LICENSE](#license) 116 | 117 | 118 | 119 | ## Installation 120 | 121 | This module is distributed via [npm][npm] which is bundled with [node][node] and 122 | should be installed as one of your project's `dependencies`: 123 | 124 | ``` 125 | npm install --save use-conditional-effect 126 | ``` 127 | 128 | ## Usage 129 | 130 | You use it in place of `React.useEffect`. 131 | 132 | Example: 133 | 134 | ```jsx 135 | import React from 'react' 136 | import ReactDOM from 'react-dom' 137 | import useConditionalEffect from 'use-conditional-effect' 138 | 139 | function Query({query, variables}) { 140 | // Example: using some external library's method 141 | const getQueryResult = useMyGraphQlLibrary() 142 | 143 | // We don't need to use an array for the second argument 144 | // The third argument is the comparison function 145 | useConditionalEffect( 146 | () => { 147 | getQueryResult(query, variables) 148 | }, 149 | {query, variables, getQueryResult}, 150 | (current, previous = {}) => { 151 | if ( 152 | current.query !== previous.query || 153 | current.variables.username !== previous.variables.username 154 | ) { 155 | return true 156 | } 157 | }, 158 | ) 159 | 160 | return
{/* awesome UI here */}
161 | } 162 | ``` 163 | 164 | ### Compatibility with `React.useEffect` 165 | 166 | - If you don't pass the third argument, the comparison function defaults to the 167 | same comparison function as useEffect (thus, the second argument has to be an 168 | array in this case). 169 | - If you don't pass the second or third arguments, the effect always runs (same 170 | as useEffect). 171 | 172 | ## Other Solutions 173 | 174 | - https://github.com/kentcdodds/use-deep-compare-effect 175 | 176 | ## LICENSE 177 | 178 | MIT 179 | 180 | [npm]: https://www.npmjs.com/ 181 | [node]: https://nodejs.org 182 | [build-badge]: 183 | https://img.shields.io/travis/alexkrolick/use-conditional-effect.svg?style=flat-square 184 | [build]: https://travis-ci.org/alexkrolick/use-conditional-effect 185 | [coverage-badge]: 186 | https://img.shields.io/codecov/c/github/alexkrolick/use-conditional-effect.svg?style=flat-square 187 | [coverage]: https://codecov.io/github/alexkrolick/use-conditional-effect 188 | [version-badge]: 189 | https://img.shields.io/npm/v/use-conditional-effect.svg?style=flat-square 190 | [package]: https://www.npmjs.com/package/use-conditional-effect 191 | [downloads-badge]: 192 | https://img.shields.io/npm/dm/use-conditional-effect.svg?style=flat-square 193 | [npmtrends]: http://www.npmtrends.com/use-conditional-effect 194 | [license-badge]: 195 | https://img.shields.io/npm/l/use-conditional-effect.svg?style=flat-square 196 | [license]: 197 | https://github.com/alexkrolick/use-conditional-effect/blob/master/LICENSE 198 | [prs-badge]: 199 | https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 200 | [prs]: http://makeapullrequest.com 201 | [donate-badge]: 202 | https://img.shields.io/badge/$-support-green.svg?style=flat-square 203 | [coc-badge]: 204 | https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square 205 | [coc]: 206 | https://github.com/alexkrolick/use-conditional-effect/blob/master/other/CODE_OF_CONDUCT.md 207 | [github-watch-badge]: 208 | https://img.shields.io/github/watchers/alexkrolick/use-conditional-effect.svg?style=social 209 | [github-watch]: https://github.com/alexkrolick/use-conditional-effect/watchers 210 | [github-star-badge]: 211 | https://img.shields.io/github/stars/alexkrolick/use-conditional-effect.svg?style=social 212 | [github-star]: https://github.com/alexkrolick/use-conditional-effect/stargazers 213 | [twitter]: 214 | https://twitter.com/intent/tweet?text=Check%20out%20use-conditional-effect%20by%20%40alexkrolick%20https%3A%2F%2Fgithub.com%2Falexkrolick%2Fuse-conditional-effect%20%F0%9F%91%8D 215 | [twitter-badge]: 216 | https://img.shields.io/twitter/url/https/github.com/alexkrolick/use-conditional-effect.svg?style=social 217 | [emojis]: https://github.com/alexkrolick/all-contributors#emoji-key 218 | [all-contributors]: https://github.com/alexkrolick/all-contributors 219 | [contributors]: 220 | https://github.com/alexkrolick/use-conditional-effect/blob/master/CONTRIBUTORS.md 221 | [useeffect]: https://reactjs.org/docs/hooks-reference.html#useeffect 222 | [exhaustive-deps-lint]: 223 | https://github.com/facebook/react/issues/14920#issuecomment-471070149 224 | -------------------------------------------------------------------------------- /other/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alexander.krolick+github@gmail.com. 59 | All complaints will be reviewed and investigated and will result in a response 60 | that is deemed necessary and appropriate to the circumstances. The project team 61 | is obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 73 | 74 | [homepage]: http://contributor-covenant.org 75 | [version]: http://contributor-covenant.org/version/1/4/ 76 | -------------------------------------------------------------------------------- /other/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | This is documentation for maintainers of this project. 4 | 5 | ## Code of Conduct 6 | 7 | Please review, understand, and be an example of it. Violations of the code of 8 | conduct are taken seriously, even (especially) for maintainers. 9 | 10 | ## Issues 11 | 12 | We want to support and build the community. We do that best by helping people 13 | learn to solve their own problems. We have an issue template and hopefully most 14 | folks follow it. If it's not clear what the issue is, invite them to create a 15 | minimal reproduction of what they're trying to accomplish or the bug they think 16 | they've found. 17 | 18 | Once it's determined that a code change is necessary, point people to 19 | [makeapullrequest.com](http://makeapullrequest.com) and invite them to make a 20 | pull request. If they're the one who needs the feature, they're the one who can 21 | build it. If they need some hand holding and you have time to lend a hand, 22 | please do so. It's an investment into another human being, and an investment 23 | into a potential maintainer. 24 | 25 | Remember that this is open source, so the code is not yours, it's ours. If 26 | someone needs a change in the codebase, you don't have to make it happen 27 | yourself. Commit as much time to the project as you want/need to. Nobody can ask 28 | any more of you than that. 29 | 30 | ## Pull Requests 31 | 32 | As a maintainer, you're fine to make your branches on the main repo or on your 33 | own fork. Either way is fine. 34 | 35 | When we receive a pull request, a travis build is kicked off automatically (see 36 | the `.travis.yml` for what runs in the travis build). We avoid merging anything 37 | that breaks the travis build. 38 | 39 | Please review PRs and focus on the code rather than the individual. You never 40 | know when this is someone's first ever PR and we want their experience to be as 41 | positive as possible, so be uplifting and constructive. 42 | 43 | When you merge the pull request, 99% of the time you should use the 44 | [Squash and merge](https://help.github.com/articles/merging-a-pull-request/) 45 | feature. This keeps our git history clean, but more importantly, this allows us 46 | to make any necessary changes to the commit message so we release what we want 47 | to release. See the next section on Releases for more about that. 48 | 49 | ## Release 50 | 51 | Our releases are automatic. They happen whenever code lands into `master`. A 52 | travis build gets kicked off and if it's successful, a tool called 53 | [`semantic-release`](https://github.com/semantic-release/semantic-release) is 54 | used to automatically publish a new release to npm as well as a changelog to 55 | GitHub. It is only able to determine the version and whether a release is 56 | necessary by the git commit messages. With this in mind, **please brush up on 57 | [the commit message convention][commit] which drives our releases.** 58 | 59 | > One important note about this: Please make sure that commit messages do NOT 60 | > contain the words "BREAKING CHANGE" in them unless we want to push a major 61 | > version. I've been burned by this more than once where someone will include 62 | > "BREAKING CHANGE: None" and it will end up releasing a new major version. Not 63 | > a huge deal honestly, but kind of annoying... 64 | 65 | ## Thanks! 66 | 67 | Thank you so much for helping to maintain this project! 68 | 69 | [commit]: 70 | https://github.com/conventional-changelog-archived-repos/conventional-changelog-angular/blob/ed32559941719a130bb0327f886d6a32a8cbc2ba/convention.md 71 | -------------------------------------------------------------------------------- /other/USERS.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | If you or your company uses this project, add your name to this list! Eventually 4 | we may have a website to showcase these (wanna build it!?) 5 | 6 | > No users have been added yet! 7 | 8 | 13 | -------------------------------------------------------------------------------- /other/manual-releases.md: -------------------------------------------------------------------------------- 1 | # manual-releases 2 | 3 | This project has an automated release set up. So things are only released when 4 | there are useful changes in the code that justify a release. But sometimes 5 | things get messed up one way or another and we need to trigger the release 6 | ourselves. When this happens, simply bump the number below and commit that with 7 | the following commit message based on your needs: 8 | 9 | **Major** 10 | 11 | ``` 12 | fix(release): manually release a major version 13 | 14 | There was an issue with a major release, so this manual-releases.md 15 | change is to release a new major version. 16 | 17 | Reference: # 18 | 19 | BREAKING CHANGE: 20 | ``` 21 | 22 | **Minor** 23 | 24 | ``` 25 | feat(release): manually release a minor version 26 | 27 | There was an issue with a minor release, so this manual-releases.md 28 | change is to release a new minor version. 29 | 30 | Reference: # 31 | ``` 32 | 33 | **Patch** 34 | 35 | ``` 36 | fix(release): manually release a patch version 37 | 38 | There was an issue with a patch release, so this manual-releases.md 39 | change is to release a new patch version. 40 | 41 | Reference: # 42 | ``` 43 | 44 | The number of times we've had to do a manual release is: 0 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-conditional-effect", 3 | "version": "0.0.0-semantically-released", 4 | "description": "Replacement for React.useEffect with optional comparison function", 5 | "main": "dist/use-conditional-effect.cjs.js", 6 | "module": "dist/use-conditional-effect.esm.js", 7 | "engines": { 8 | "node": "> 4", 9 | "npm": "> 3" 10 | }, 11 | "scripts": { 12 | "add-contributor": "kcd-scripts contributors add", 13 | "build": "kcd-scripts build --bundle", 14 | "lint": "kcd-scripts lint", 15 | "test": "kcd-scripts test", 16 | "test:update": "npm test -- --updateSnapshot --coverage", 17 | "validate": "kcd-scripts validate", 18 | "setup": "npm install && npm run validate -s" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "keywords": [], 24 | "author": "Alex Krolick (https://alexkrolick.com/)", 25 | "license": "MIT", 26 | "peerDependencies": { 27 | "react": ">=16.8" 28 | }, 29 | "dependencies": {}, 30 | "devDependencies": { 31 | "@babel/runtime": "^7.4.5", 32 | "kcd-scripts": "^1.4.0", 33 | "react": "^16.8.6", 34 | "react-dom": "^16.8.6", 35 | "react-hooks-testing-library": "0.3.5", 36 | "react-test-renderer": "^16.8.6" 37 | }, 38 | "husky": { 39 | "hooks": { 40 | "pre-commit": "kcd-scripts pre-commit" 41 | } 42 | }, 43 | "eslintConfig": { 44 | "extends": "./node_modules/kcd-scripts/eslint.js" 45 | }, 46 | "eslintIgnore": [ 47 | "node_modules", 48 | "coverage", 49 | "dist" 50 | ], 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/alexkrolick/use-conditional-effect.git" 54 | }, 55 | "bugs": { 56 | "url": "https://github.com/alexkrolick/use-conditional-effect/issues" 57 | }, 58 | "homepage": "https://github.com/alexkrolick/use-conditional-effect#readme" 59 | } 60 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import {renderHook, cleanup} from 'react-hooks-testing-library' 2 | import useConditionalEffect from '../' 3 | 4 | afterEach(cleanup) 5 | 6 | describe('compatible with useEffect', () => { 7 | test('shallow compares dependency array values', () => { 8 | const callback = jest.fn() 9 | let deps = [1, 2] 10 | 11 | const {rerender} = renderHook(() => useConditionalEffect(callback, deps)) 12 | 13 | expect(callback).toHaveBeenCalledTimes(1) 14 | callback.mockClear() 15 | 16 | deps = [2, 1] 17 | rerender() 18 | expect(callback).toHaveBeenCalledTimes(1) 19 | callback.mockClear() 20 | 21 | deps = [2, 1] 22 | rerender() 23 | expect(callback).toHaveBeenCalledTimes(0) 24 | callback.mockClear() 25 | 26 | deps = [2, 1, {a: 1}] 27 | rerender() 28 | expect(callback).toHaveBeenCalledTimes(1) 29 | callback.mockClear() 30 | }) 31 | 32 | test('always runs with no dependencies are passed', () => { 33 | const callback = jest.fn() 34 | const {rerender} = renderHook(() => useConditionalEffect(callback)) 35 | 36 | expect(callback).toHaveBeenCalledTimes(1) 37 | callback.mockClear() 38 | 39 | rerender() 40 | expect(callback).toHaveBeenCalledTimes(1) 41 | callback.mockClear() 42 | }) 43 | }) 44 | 45 | describe('custom comparison function', () => { 46 | test('uses custom comparison function when provided', () => { 47 | const callback = jest.fn() 48 | let deps = [1] 49 | const compare = jest.fn((a, b) => Object.is(a, b)) 50 | 51 | const {rerender} = renderHook(() => 52 | useConditionalEffect(callback, deps, compare), 53 | ) 54 | 55 | expect(compare).toHaveBeenCalledTimes(1) 56 | expect(callback).toHaveBeenCalledTimes(1) 57 | callback.mockClear() 58 | 59 | // expect the effect to run here because the custom 60 | // comparison uses object identity on the whole array 61 | // instead of shallow comparison on the array values like the default 62 | deps = [1] 63 | rerender() 64 | expect(callback).toHaveBeenCalledTimes(1) 65 | callback.mockClear() 66 | }) 67 | 68 | test('example: identity comparison', () => { 69 | const callback = jest.fn() 70 | let deps = [1] 71 | const compare = jest.fn((a, b) => Object.is(a, b)) 72 | 73 | const {rerender} = renderHook(() => 74 | useConditionalEffect(callback, deps, compare), 75 | ) 76 | 77 | expect(compare).toHaveBeenCalledTimes(1) 78 | expect(callback).toHaveBeenCalledTimes(1) 79 | callback.mockClear() 80 | 81 | // expect the effect to run here because the custom 82 | // comparison uses object identity on the whole array 83 | // instead of shallow comparison on the array values like the default 84 | deps = [1] 85 | rerender() 86 | expect(callback).toHaveBeenCalledTimes(1) 87 | callback.mockClear() 88 | }) 89 | 90 | test('example: deep comparison', () => { 91 | const callback = jest.fn() 92 | // note we can use an object for easier lookup 93 | let deps = { 94 | query: 'search', 95 | variables: {username: 'ak'}, 96 | } 97 | const shouldRunEffect = (current, previous) => { 98 | if (!previous) return true 99 | if ( 100 | current.query !== previous.query && 101 | current.variables.username !== previous.variables.username 102 | ) { 103 | return true 104 | } 105 | return false 106 | } 107 | 108 | const {rerender} = renderHook(() => 109 | useConditionalEffect(callback, deps, shouldRunEffect), 110 | ) 111 | 112 | expect(callback).toHaveBeenCalledTimes(1) 113 | callback.mockClear() 114 | 115 | deps = { 116 | query: 'search', 117 | variables: {username: 'someone_else'}, 118 | } 119 | rerender() 120 | expect(callback).toHaveBeenCalledTimes(1) 121 | callback.mockClear() 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable react-hooks/exhaustive-deps 2 | 3 | import React from 'react' 4 | 5 | function shallowEquals(a, b) { 6 | let isEqual = true 7 | // technically equal but we always run effects with there are no dependencies 8 | if (a === undefined && b === undefined) { 9 | isEqual = false 10 | // compare array *values* and length 11 | } else if (Array.isArray(a) && Array.isArray(b)) { 12 | for (const i in a) { 13 | if (!Object.is(a[i], b[i])) isEqual = false 14 | } 15 | if (a.length !== b.length) isEqual = false 16 | // compare everything else by identity 17 | } else { 18 | isEqual = Object.is(a, b) 19 | } 20 | return isEqual 21 | } 22 | 23 | function getArtificialDependency() { 24 | return {} 25 | } 26 | 27 | function useConditionalEffect(callback, dependencies, isEqual = shallowEquals) { 28 | const lastDependenciesValue = React.useRef(undefined) 29 | const artificialDep = React.useRef(getArtificialDependency()) 30 | const shouldRunEffect = !isEqual(dependencies, lastDependenciesValue.current) 31 | React.useEffect( 32 | callback, 33 | shouldRunEffect 34 | ? [(artificialDep.current = getArtificialDependency())] 35 | : [artificialDep.current], 36 | ) 37 | lastDependenciesValue.current = dependencies 38 | } 39 | 40 | export default useConditionalEffect 41 | --------------------------------------------------------------------------------