diff --git a/README.md b/README.md
index cf43944..6ab3e0e 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,24 @@ export default track({
})(FooPage);
```
+### Usage with React Hooks
+
+Following the example above, once a component is wrapped with `track` we can access a `tracking` object via the `useTracking` hook from anywhere in the sub-tree:
+
+```js
+import { useTracking } from 'react-tracking'
+
+const SomeChild = props => {
+ const tracking = useTracking()
+
+
{
+ tracking.trackEvent({ action: 'click' });
+ }}
+ />
+}
+```
+
This is also how you would use this module without `@decorators`, although this is obviously awkward and the decorator syntax is recommended.
### Custom `options.dispatch()` for tracking data
diff --git a/package-lock.json b/package-lock.json
index 68e7916..aaaa3a1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3931,7 +3931,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"aproba": {
"version": "1.2.0",
@@ -3952,12 +3953,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -3972,17 +3975,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -4099,7 +4105,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -4111,6 +4118,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -4125,6 +4133,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -4132,12 +4141,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -4156,6 +4167,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -4236,7 +4248,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -4248,6 +4261,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
@@ -4333,7 +4347,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -4369,6 +4384,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -4388,6 +4404,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
+ "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -4431,12 +4448,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
- "dev": true
+ "dev": true,
+ "optional": true
}
}
},
diff --git a/package.json b/package.json
index 8010ced..4cf7e18 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,7 @@
},
"peerDependencies": {
"core-js": "2.x",
- "react": "^16.3",
+ "react": "^16.8",
"prop-types": "^15.x"
},
"devDependencies": {
diff --git a/src/__tests__/e2e.test.js b/src/__tests__/e2e.test.js
index dc59c0e..a5ef64e 100644
--- a/src/__tests__/e2e.test.js
+++ b/src/__tests__/e2e.test.js
@@ -1,5 +1,5 @@
/* eslint-disable react/destructuring-assignment,react/no-multi-comp,react/prop-types,react/prefer-stateless-function */
-import React from 'react';
+import React, { useContext } from 'react';
import { mount } from 'enzyme';
const dispatchTrackingEvent = jest.fn();
@@ -12,13 +12,13 @@ const testState = { booleanState: true };
describe('e2e', () => {
// eslint-disable-next-line global-require
- const track = require('../').default;
+ const { default: track, useTracking, ReactTrackingContext } = require('../');
beforeEach(() => {
jest.clearAllMocks();
});
- it('defaults moslty everything', () => {
+ it('defaults mostly everything', () => {
@track(null, { process: () => null })
class TestDefaults extends React.Component {
render() {
@@ -584,4 +584,60 @@ describe('e2e', () => {
page: 'Page',
});
});
+
+ it('root context items are accessible to children', () => {
+ const App = track()(() => {
+ return
;
+ });
+
+ const Child = () => {
+ const trackingContext = useContext(ReactTrackingContext);
+ expect(Object.keys(trackingContext.tracking)).toEqual([
+ 'data',
+ 'dispatch',
+ 'process',
+ ]);
+ return
;
+ };
+
+ mount(
);
+ });
+
+ it('dispatches tracking events from a useTracking hook tracking object', () => {
+ const outerTrackingData = {
+ page: 'Page',
+ };
+
+ const Page = track(outerTrackingData, { dispatch })(props => {
+ return props.children;
+ });
+
+ const Child = () => {
+ const tracking = useTracking();
+
+ expect(tracking.getTrackingData()).toEqual(outerTrackingData);
+
+ return (
+
{
+ tracking.trackEvent({ event: 'buttonClick' });
+ }}
+ />
+ );
+ };
+
+ const wrappedApp = mount(
+
+
+
+ );
+
+ wrappedApp.find('button').simulate('click');
+
+ expect(dispatch).toHaveBeenCalledWith({
+ ...outerTrackingData,
+ event: 'buttonClick',
+ });
+ });
});
diff --git a/src/__tests__/useTracking.test.js b/src/__tests__/useTracking.test.js
new file mode 100644
index 0000000..0cb028f
--- /dev/null
+++ b/src/__tests__/useTracking.test.js
@@ -0,0 +1,55 @@
+import { mount } from 'enzyme';
+import React from 'react';
+import { renderToString } from 'react-dom/server';
+import track from '../withTrackingComponentDecorator';
+import useTracking from '../useTracking';
+
+describe('useTracking', () => {
+ it('throws error if tracking context not present', () => {
+ const ThrowMissingContext = () => {
+ useTracking();
+ return hi
;
+ };
+ try {
+ renderToString( );
+ } catch (error) {
+ expect(error.message).toContain(
+ 'Attempting to call `useTracking` without a ReactTrackingContext present'
+ );
+ }
+ });
+
+ it('dispatches tracking events from a useTracking hook tracking object', () => {
+ const outerTrackingData = {
+ page: 'Page',
+ };
+
+ const dispatch = jest.fn();
+
+ const App = track(outerTrackingData, { dispatch })(() => {
+ const tracking = useTracking();
+
+ expect(tracking.getTrackingData()).toEqual({
+ page: 'Page',
+ });
+
+ return (
+
+ tracking.trackEvent({
+ event: 'buttonClick',
+ })
+ }
+ />
+ );
+ });
+
+ const wrapper = mount( );
+ wrapper.simulate('click');
+ expect(dispatch).toHaveBeenCalledWith({
+ ...outerTrackingData,
+ event: 'buttonClick',
+ });
+ });
+});
diff --git a/src/index.js b/src/index.js
index 5376159..20abda9 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,4 +5,5 @@ export {
} from './withTrackingComponentDecorator';
export { default as trackEvent } from './trackEventMethodDecorator';
export { default as TrackingPropType } from './TrackingPropType';
+export { default as useTracking } from './useTracking';
export { default } from './trackingHoC';
diff --git a/src/useTracking.js b/src/useTracking.js
new file mode 100644
index 0000000..43eceae
--- /dev/null
+++ b/src/useTracking.js
@@ -0,0 +1,29 @@
+import merge from 'deepmerge';
+import { useContext, useCallback } from 'react';
+import { ReactTrackingContext } from './withTrackingComponentDecorator';
+
+export default function useTracking() {
+ const trackingContext = useContext(ReactTrackingContext);
+
+ if (!(trackingContext && trackingContext.tracking)) {
+ throw new Error(
+ 'Attempting to call `useTracking` ' +
+ 'without a ReactTrackingContext present. Did you forget to wrap the top of ' +
+ 'your component tree with `track`?'
+ );
+ }
+
+ const trackEvent = useCallback(
+ data => {
+ return trackingContext.tracking.dispatch(
+ merge(trackingContext.tracking.data, data)
+ );
+ },
+ [trackingContext.tracking.data]
+ );
+
+ return {
+ getTrackingData: () => trackingContext.tracking.data,
+ trackEvent,
+ };
+}