diff --git a/.eslintrc.js b/.eslintrc.js index bb2159a..781d61d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,13 +1,15 @@ module.exports = { parser: 'babel-eslint', extends: ['airbnb', 'plugin:prettier/recommended', 'prettier/react'], - plugins: ['prettier'], + plugins: ['prettier', 'react-hooks'], env: { jest: true, browser: true, }, rules: { 'prettier/prettier': 'error', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', 'react/jsx-filename-extension': [ 1, { diff --git a/package-lock.json b/package-lock.json index 0cabdf0..512a4b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2677,9 +2677,9 @@ "dev": true }, "core-js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", - "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.3.2.tgz", + "integrity": "sha512-S1FfZpeBchkhyoY76YAdFzKS4zz9aOK7EeFaNA2aJlyXyA+sgqz6xdxmLPGXEAf0nF44MVN1kSjrA9Kt3ATDQg==" }, "core-js-compat": { "version": "3.2.1", @@ -3124,18 +3124,18 @@ } }, "enzyme-adapter-react-16": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.0.tgz", - "integrity": "sha512-p5k5TAG9hiyFNgJ7ABkfg5Poc3Gp5D2uArDEv7BW/FE0AflqIRfHFi4G3Ei+MpPuwy5Ao+ZisYWKuxC5LRCr1Q==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz", + "integrity": "sha512-yMPxrP3vjJP+4wL/qqfkT6JAIctcwKF+zXO6utlGPgUJT2l4tzrdjMDWGd/Pp1BjHBcljhN24OzNEGRteibJhA==", "dev": true, "requires": { - "enzyme-adapter-utils": "^1.12.0", + "enzyme-adapter-utils": "^1.12.1", "enzyme-shallow-equal": "^1.0.0", "has": "^1.0.3", "object.assign": "^4.1.0", "object.values": "^1.1.0", "prop-types": "^15.7.2", - "react-is": "^16.8.6", + "react-is": "^16.10.2", "react-test-renderer": "^16.0.0-0", "semver": "^5.7.0" }, @@ -3513,6 +3513,12 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.1.2.tgz", + "integrity": "sha512-ZR+AyesAUGxJAyTFlF3MbzeVHAcQTFQt1fFVe5o0dzY/HFoj1dgQDMoIkiM+ltN/HhlHBYX4JpJwYonjxsyQMA==", + "dev": true + }, "eslint-scope": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", @@ -4875,20 +4881,20 @@ } }, "husky": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.8.tgz", - "integrity": "sha512-HFOsgcyrX3qe/rBuqyTt+P4Gxn5P0seJmr215LAZ/vnwK3jWB3r0ck7swbzGRUbufCf9w/lgHPVbF/YXQALgfQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.9.tgz", + "integrity": "sha512-Yolhupm7le2/MqC1VYLk/cNmYxsSsqKkTyBhzQHhPK1jFnC89mmmNVuGtLNabjDI6Aj8UNIr0KpRNuBkiC4+sg==", "dev": true, "requires": { "chalk": "^2.4.2", + "ci-info": "^2.0.0", "cosmiconfig": "^5.2.1", "execa": "^1.0.0", "get-stdin": "^7.0.0", - "is-ci": "^2.0.0", "opencollective-postinstall": "^2.0.2", "pkg-dir": "^4.2.0", "please-upgrade-node": "^3.2.0", - "read-pkg": "^5.1.1", + "read-pkg": "^5.2.0", "run-node": "^1.0.0", "slash": "^3.0.0" }, diff --git a/package.json b/package.json index a91c319..c8ecf97 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ ] }, "dependencies": { - "core-js": "^3.2.1", + "core-js": "^3.3.2", "deepmerge": "^4.1.1", "hoist-non-react-statics": "^3.3.0" }, @@ -86,7 +86,7 @@ "babel-eslint": "^10.0.3", "babel-plugin-transform-decorators-legacy": "^1.3.5", "enzyme": "^3.10.0", - "enzyme-adapter-react-16": "^1.15.0", + "enzyme-adapter-react-16": "^1.15.1", "eslint": "^6.5.1", "eslint-config-airbnb": "^18.0.1", "eslint-config-prettier": "^6.4.0", @@ -94,7 +94,8 @@ "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-prettier": "^3.1.1", "eslint-plugin-react": "^7.16.0", - "husky": "^3.0.8", + "eslint-plugin-react-hooks": "^2.1.2", + "husky": "^3.0.9", "jest": "^24.9.0", "lint-staged": "^9.4.2", "prettier": "^1.18.2", diff --git a/src/withTrackingComponentDecorator.js b/src/withTrackingComponentDecorator.js index 7ddc7b0..3de14a5 100644 --- a/src/withTrackingComponentDecorator.js +++ b/src/withTrackingComponentDecorator.js @@ -1,5 +1,11 @@ /* eslint-disable react/jsx-props-no-spreading */ -import React, { useCallback, useContext, useEffect, useMemo } from 'react'; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, +} from 'react'; import PropTypes from 'prop-types'; import merge from 'deepmerge'; import hoistNonReactStatic from 'hoist-non-react-statics'; @@ -24,16 +30,26 @@ export default function withTrackingComponentDecorator( function WithTracking(props) { const { tracking } = useContext(ReactTrackingContext); + const latestProps = useRef(props); - const getProcessFn = useCallback(() => tracking && tracking.process, []); + useEffect(() => { + // keep the latest props in a mutable ref object to avoid creating + // additional dependency that could cause unnecessary re-renders + // see https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often + latestProps.current = props; + }); + + // statically extract tracking.process for hook dependency + const trkProcess = tracking && tracking.process; + const getProcessFn = useCallback(() => trkProcess, [trkProcess]); const getOwnTrackingData = useCallback(() => { const ownTrackingData = typeof trackingData === 'function' - ? trackingData(props) + ? trackingData(latestProps.current) : trackingData; return ownTrackingData || {}; - }, [trackingData, props]); + }, []); const getTrackingDataFn = useCallback(() => { const contextGetTrackingData = @@ -43,12 +59,12 @@ export default function withTrackingComponentDecorator( contextGetTrackingData === getOwnTrackingData ? getOwnTrackingData() : merge(contextGetTrackingData(), getOwnTrackingData()); - }, [getOwnTrackingData]); + }, [getOwnTrackingData, tracking]); const getTrackingDispatcher = useCallback(() => { const contextDispatch = (tracking && tracking.dispatch) || dispatch; return data => contextDispatch(merge(getOwnTrackingData(), data || {})); - }, [dispatch, getOwnTrackingData]); + }, [getOwnTrackingData, tracking]); const trackEvent = useCallback( (data = {}) => { @@ -88,7 +104,7 @@ export default function withTrackingComponentDecorator( } else if (dispatchOnMount === true) { trackEvent(); } - }, []); + }, [getOwnTrackingData, getProcessFn, getTrackingDataFn, trackEvent]); const trackingProp = useMemo( () => ({ @@ -106,7 +122,7 @@ export default function withTrackingComponentDecorator( process: getProcessFn() || process, }, }), - [getTrackingDispatcher, getTrackingDataFn, getProcessFn, process] + [getTrackingDispatcher, getTrackingDataFn, getProcessFn] ); return useMemo( @@ -115,7 +131,7 @@ export default function withTrackingComponentDecorator( ), - [contextValue, trackingProp] + [contextValue, props, trackingProp] ); }