├── .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 |
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 | [][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 |
--------------------------------------------------------------------------------