diff --git a/app/layout.tsx b/app/layout.tsx index be5ecd1..2ffe0e3 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,14 +1,15 @@ import { draftMode } from 'next/headers'; import { VercelToolbar } from '@vercel/toolbar/next'; +import { AnalyticsComponent } from 'components/analytics'; +import { graphql } from 'gql.tada'; -import { graphqlClient } from '#/lib/graphqlClient'; +import { ContentfulPreviewProvider } from '#/components/contentful-preview-provider'; -import './globals.css'; +import { graphqlClient } from '../lib/graphqlClient'; -import { graphql } from 'gql.tada'; +import './globals.css'; -import { ContentfulPreviewProvider } from '#/components/contentful-preview-provider'; import { NavigationFieldsFragment } from '#/components/navigation'; import { SiteHeader } from '#/components/site-header'; import { isContentSourceMapsEnabled } from '#/lib/contentSourceMaps'; @@ -47,13 +48,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo */} - -
- -
{children}
- {shouldInjectToolbar && } -
-
+ + +
+ +
{children}
+ {shouldInjectToolbar && } +
+
+
); diff --git a/components/analytics/README.md b/components/analytics/README.md new file mode 100644 index 0000000..dc0b55b --- /dev/null +++ b/components/analytics/README.md @@ -0,0 +1,44 @@ +# Analytics + +Analytics solution is based on the [getanalytics.io](https://getanalytics.io/) library. +The library provides three base API interfaces to sent tracking information: +- `page()` - trigger page view. This will trigger page calls in any installed plugins +- `identify()` - this will trigger identify calls in any installed plugins and will set user data in localStorage +- `track()` - Track an analytics event. This will trigger track calls in any installed plugins + +## NextJS Integration + +We will provide integration with NextJS for three different cases: page view, +component in view, click on target. See the `components/analytics/analytics.tsx` +file where we convey the global Analytics context and define a hook to track +page view. Then go to the `app/layout.tsx` where we wrap all children components +inside the analytics context. + +### Page view tracking + +Page view will automatically triggered after the page will be loaded depending +on the NextJS router `pathname` changes. + +### Component InView tracking + +We introduced the wrapper component `TrackInView` that can be used to wrap any +client component and send analytics tracking event when the component is fully viewed. + +### On Click analytics + +Here's the place where we should communicate with the UI component and current +implementation suppose just passing the click tracking event callback to the UI +component and then the passed callback can be attached to any of the elements +inside UI components on demand. + +## Type Safe Events + +We provide interfaces for each event in the `components/analytics/tracking-events.ts`. +Each event should be registered in the `EventsMap` interface and for each event +data should be provided own interface that describes the event data modal and +will be the value of type the registered event in the `EventsMap` +Also that file provides a helper function `createAnalyticsEvent()` that should be +used to create any analytics event on the client level. This function will accept +the event name string as the first argument and the event data object as the +second argument and map given event name to the corresponding event data type. +It will guarantee that all events will have correct data. diff --git a/components/analytics/analytics.tsx b/components/analytics/analytics.tsx new file mode 100644 index 0000000..10f9c01 --- /dev/null +++ b/components/analytics/analytics.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { PropsWithChildren, useEffect } from 'react'; +import { usePathname } from 'next/navigation'; + +import Analytics from 'analytics'; +import { AnalyticsProvider } from 'use-analytics'; + +const analyticsInstance = Analytics({ + app: 'starterkit', + debug: true, +}); + +export function AnalyticsComponent({ children }: PropsWithChildren) { + const pathname = usePathname(); + + useEffect(() => { + analyticsInstance.page(); + }, [pathname]); + + return {children}; +} diff --git a/components/analytics/index.ts b/components/analytics/index.ts new file mode 100644 index 0000000..77dc4f5 --- /dev/null +++ b/components/analytics/index.ts @@ -0,0 +1 @@ +export * from './analytics'; diff --git a/components/analytics/trackInView.tsx b/components/analytics/trackInView.tsx new file mode 100644 index 0000000..839c360 --- /dev/null +++ b/components/analytics/trackInView.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { InView } from 'react-intersection-observer'; +import { useAnalytics } from 'use-analytics'; + +import { EventData, EventName } from './tracking-events'; + +interface TrackInViewProps { + eventName: EventName; + eventData: EventData; + children: React.ReactNode; +} + +export const TrackInView = ({ eventName, eventData, children }: TrackInViewProps) => { + const { track } = useAnalytics(); + const onComponentIntersection = (inView: boolean) => { + if (inView) { + track(eventName, eventData); + } + }; + return ( + onComponentIntersection(inView)}> + {children} + + ); +}; diff --git a/components/analytics/tracking-events.ts b/components/analytics/tracking-events.ts new file mode 100644 index 0000000..a5beced --- /dev/null +++ b/components/analytics/tracking-events.ts @@ -0,0 +1,26 @@ +interface EventsMap { + heroBannerViewed: heroBannerViewedProps; + duplexViewed: duplexViewedProps; + duplexClicked: duplexClickedProps; +} + +interface heroBannerViewedProps { + category: string; +} + +interface duplexViewedProps { + category: string; + type: string; +} + +interface duplexClickedProps extends duplexViewedProps {} + +export type EventName = keyof EventsMap; +export type EventData = EventsMap[T]; + +export function createAnalyticsEvent(eventName: T, eventData: EventData) { + return { + eventName, + eventData, + }; +} diff --git a/components/duplex-ctf/duplex-ctf-client.tsx b/components/duplex-ctf/duplex-ctf-client.tsx index bb2ec7d..7b3d52a 100644 --- a/components/duplex-ctf/duplex-ctf-client.tsx +++ b/components/duplex-ctf/duplex-ctf-client.tsx @@ -1,7 +1,10 @@ 'use client'; import { ResultOf } from 'gql.tada'; +import { useAnalytics } from 'use-analytics'; +import { createAnalyticsEvent } from '#/components/analytics/tracking-events'; +import { TrackInView } from '#/components/analytics/trackInView'; import { RichTextCtf } from '#/components/rich-text-ctf'; import { useComponentPreview } from '../hooks/use-component-preview'; @@ -10,41 +13,62 @@ import { getPageLinkChildProps } from '../page'; import { Duplex } from '../ui/duplex'; import { ComponentDuplexFieldsFragment } from './duplex-ctf'; +// We can create analytics event typed data on top level +// using createAnalyticsEvent helper. +// It will us type-safely create event name and event data. +const analyticsInViewEvent = createAnalyticsEvent('duplexViewed', { + category: 'duplexViewed', + type: 'ctf', +}); + +// For the direct track() usage from hook we can destructure the object +// returned from createAnalyticsEvent helper to have separate values as props. +const { eventName: analyticsClickEventName, eventData: analyticsClickEventData } = createAnalyticsEvent( + 'duplexClicked', + { + category: 'duplexClicked', + type: 'ctf', + } +); + export const DuplexCtfClient: React.FC<{ data: ResultOf; }> = (props) => { const { data: originalData } = props; const { data, addAttributes } = useComponentPreview(originalData); - + const { track } = useAnalytics(); return ( - - - - ) - } - image={ - data.image && - getImageChildProps({ - data: data.image, - priority: true, - sizes: '100vw', - }) - } - imageAlignment={data.containerLayout ? 'left' : 'right'} - imageHeight={data.imageStyle ? 'fixed' : 'full'} - addAttributes={addAttributes} - cta={ - data.targetPage && - getPageLinkChildProps({ - data: data.targetPage, - children: data.ctaText, - }) - } - colorPalette={data.colorPalette} - /> + + + + + ) + } + image={ + data.image && + getImageChildProps({ + data: data.image, + priority: true, + sizes: '100vw', + }) + } + imageAlignment={data.containerLayout ? 'left' : 'right'} + imageHeight={data.imageStyle ? 'fixed' : 'full'} + addAttributes={addAttributes} + cta={ + data.targetPage && + getPageLinkChildProps({ + data: data.targetPage, + children: data.ctaText, + }) + } + colorPalette={data.colorPalette} + onClickAnalyticsEvent={() => track(analyticsClickEventName, analyticsClickEventData)} + /> + ); }; diff --git a/components/hero-banner-ctf/hero-banner-ctf-client.tsx b/components/hero-banner-ctf/hero-banner-ctf-client.tsx index 3597cda..02dc82f 100644 --- a/components/hero-banner-ctf/hero-banner-ctf-client.tsx +++ b/components/hero-banner-ctf/hero-banner-ctf-client.tsx @@ -2,42 +2,49 @@ import { ResultOf } from 'gql.tada'; +import { createAnalyticsEvent } from '#/components/analytics/tracking-events'; +import { TrackInView } from '#/components/analytics/trackInView'; +import { ComponentHeroBannerFieldsFragment } from '#/components/hero-banner-ctf/hero-banner-ctf'; import { RichTextCtf } from '#/components/rich-text-ctf'; import { useComponentPreview } from '../hooks/use-component-preview'; import { getImageChildProps } from '../image-ctf'; import { getPageLinkChildProps } from '../page'; import { HeroBanner } from '../ui/hero-banner'; -import { ComponentHeroBannerFieldsFragment } from './hero-banner-ctf'; export const HeroBannerCtfClient: React.FC<{ data: ResultOf; }> = (props) => { const { data: originalData } = props; const { data, addAttributes } = useComponentPreview(originalData); - + // We use createAnalyticsEvent helper to create typed event. + const analyticsInViewEvent = createAnalyticsEvent('heroBannerViewed', { + category: 'duplexViewed', + }); return ( - } - cta={ - data.targetPage && - getPageLinkChildProps({ - data: data.targetPage, - children: data.ctaText, - }) - } - image={ - data.image && - getImageChildProps({ - data: data.image, - sizes: '100vw', - priority: true, - }) - } - size={data.heroSize} - colorPalette={data.colorPalette} - addAttributes={addAttributes} - /> + + } + cta={ + data.targetPage && + getPageLinkChildProps({ + data: data.targetPage, + children: data.ctaText, + }) + } + image={ + data.image && + getImageChildProps({ + data: data.image, + sizes: '100vw', + priority: true, + }) + } + size={data.heroSize} + colorPalette={data.colorPalette} + addAttributes={addAttributes} + /> + ); }; diff --git a/components/ui/duplex/duplex.tsx b/components/ui/duplex/duplex.tsx index cf6e110..6972f0a 100644 --- a/components/ui/duplex/duplex.tsx +++ b/components/ui/duplex/duplex.tsx @@ -54,6 +54,7 @@ interface DuplexProps extends VariantProps, VariantProps< cta?: LinkProps | null; colorPalette?: string | null; addAttributes?: (name: string) => object | null; + onClickAnalyticsEvent?: () => void; } export function Duplex(props: DuplexProps) { @@ -66,6 +67,7 @@ export function Duplex(props: DuplexProps) { imageHeight, colorPalette, addAttributes = () => ({}), // Default to no-op. + onClickAnalyticsEvent, } = props; const colorConfig = getColorConfigFromPalette(colorPalette || ''); @@ -95,7 +97,14 @@ export function Duplex(props: DuplexProps) { )} {cta?.href && cta?.children && (
-
diff --git a/package.json b/package.json index 522636d..04d8f19 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@types/node": "18.11.11", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", + "analytics": "^0.8.11", "@urql/exchange-persisted": "^4.2.0", "@vercel/toolbar": "^0.1.22", "class-variance-authority": "^0.7.0", @@ -39,10 +40,12 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "server-only": "^0.0.1", + "react-intersection-observer": "^9.8.1", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.6", "typescript": "^5.4.3", - "urql": "^4.0.7" + "urql": "^4.0.7", + "use-analytics": "^1.1.0" }, "devDependencies": { "@0no-co/graphqlsp": "^1.7.0", @@ -59,6 +62,7 @@ "@storybook/nextjs": "^8.0.4", "@storybook/react": "^8.0.4", "@types/cypress-image-snapshot": "^3.1.6", + "@types/use-analytics": "^0.0.3", "@typescript-eslint/eslint-plugin": "^6.5.0", "autoprefixer": "^10.4.14", "axe-core": "^4.6.1", diff --git a/yarn.lock b/yarn.lock index e4d334f..271c5f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -39,6 +39,59 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@analytics/cookie-utils@^0.2.12": + version "0.2.12" + resolved "https://registry.yarnpkg.com/@analytics/cookie-utils/-/cookie-utils-0.2.12.tgz#acc38dd76ead968050776fb8e57e571e6d37cbc7" + integrity sha512-2h/yuIu3kmu+ZJlKmlT6GoRvUEY2k1BbQBezEv5kGhnn9KpmzPz715Y3GmM2i+m7Y0QmBdVUoA260dQZkofs2A== + dependencies: + "@analytics/global-storage-utils" "^0.1.7" + +"@analytics/core@^0.12.15": + version "0.12.15" + resolved "https://registry.yarnpkg.com/@analytics/core/-/core-0.12.15.tgz#e363cdc681d419d27b8170ce286e095f212e3576" + integrity sha512-Y+zxTNIbONXKxeEUOtcXs4b3uuiGjF5sy1zHl8ZNkIBwrOpTM8ZGNhi0xGL8ZhaQLGbi03BrT6DaoNNG3sBQOg== + dependencies: + "@analytics/global-storage-utils" "^0.1.7" + "@analytics/type-utils" "^0.6.2" + analytics-utils "^1.0.12" + +"@analytics/global-storage-utils@^0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@analytics/global-storage-utils/-/global-storage-utils-0.1.7.tgz#c6a12eb133a6e44101b7c3529c82e3e89ac9ce46" + integrity sha512-V+spzGLZYm4biZT4uefaylm80SrLXf8WOTv9hCgA46cLcyxx3LD4GCpssp1lj+RcWLl/uXJQBRO4Mnn/o1x6Gw== + dependencies: + "@analytics/type-utils" "^0.6.2" + +"@analytics/localstorage-utils@^0.1.10": + version "0.1.10" + resolved "https://registry.yarnpkg.com/@analytics/localstorage-utils/-/localstorage-utils-0.1.10.tgz#8e9b03604e79a530e9a5ab6748c8ceb96153b95c" + integrity sha512-uJS+Jp1yLG5VFCgA5T82ZODYBS0xuDQx0NtAZrgbqt9j51BX3TcgmOez5LVkrUNu/lpbxjCLq35I4TKj78VmOQ== + dependencies: + "@analytics/global-storage-utils" "^0.1.7" + +"@analytics/session-storage-utils@^0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@analytics/session-storage-utils/-/session-storage-utils-0.0.7.tgz#e355c60b14d4fbcf20983e5cfcb7cb838b4c57ab" + integrity sha512-PSv40UxG96HVcjY15e3zOqU2n8IqXnH8XvTkg1X43uXNTKVSebiI2kUjA3Q7ESFbw5DPwcLbJhV7GforpuBLDw== + dependencies: + "@analytics/global-storage-utils" "^0.1.7" + +"@analytics/storage-utils@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@analytics/storage-utils/-/storage-utils-0.4.2.tgz#222717832a533a1a2516aa3a22d5db80d14a881b" + integrity sha512-AXObwyVQw9h2uJh1t2hUgabtVxzYpW+7uKVbdHQK80vr3Td5rrmCxrCxarh7HUuAgSDZ0bZWqmYxVgmwKceaLg== + dependencies: + "@analytics/cookie-utils" "^0.2.12" + "@analytics/global-storage-utils" "^0.1.7" + "@analytics/localstorage-utils" "^0.1.10" + "@analytics/session-storage-utils" "^0.0.7" + "@analytics/type-utils" "^0.6.2" + +"@analytics/type-utils@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@analytics/type-utils/-/type-utils-0.6.2.tgz#60d706603a98a95681d4b1e9726c703fdd541a9e" + integrity sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg== + "@aw-web-design/x-default-browser@1.4.126": version "1.4.126" resolved "https://registry.yarnpkg.com/@aw-web-design/x-default-browser/-/x-default-browser-1.4.126.tgz#43e4bd8f0314ed907a8718d7e862a203af79bc16" @@ -3688,6 +3741,14 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== +"@types/use-analytics@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-analytics/-/use-analytics-0.0.3.tgz#da6770155a58d121cf816030c136ed8184004dab" + integrity sha512-s/b/R8q/pKj1INYyRWw46aI4rZFr6p0yW80EdD47FBytTGXPNJFOaPlvw2Atsrf+k6S4idl1nN5kXiLBl1MrJQ== + dependencies: + "@types/react" "*" + analytics "^0.8.1" + "@types/uuid@8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -4212,6 +4273,22 @@ ajv@^8.0.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" +analytics-utils@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/analytics-utils/-/analytics-utils-1.0.12.tgz#07bd63471d238e80f42d557fba039365f09c50db" + integrity sha512-WvV2YWgsnXLxaY0QYux0crpBAg/0JA763NmbMVz22jKhMPo7dpTBet8G2IlF7ixTjLDzGlkHk1ZaKqqQmjJ+4w== + dependencies: + "@analytics/type-utils" "^0.6.2" + dlv "^1.1.3" + +analytics@^0.8.1, analytics@^0.8.11: + version "0.8.14" + resolved "https://registry.yarnpkg.com/analytics/-/analytics-0.8.14.tgz#84b6e7e0308c8db318bea1149ea550db7e677324" + integrity sha512-ZKpqWHEHBrN0lvIsrUKmt0fcXNyQuKa0JUWDRAz7LgJ+Sf4ZX+a66/ai28W4H8kJJlLeItCrhIi/xvdbV08RlA== + dependencies: + "@analytics/core" "^0.12.15" + "@analytics/storage-utils" "^0.4.2" + ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -7701,6 +7778,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -10320,12 +10404,17 @@ react-element-to-jsx-string@^15.0.0: is-plain-object "5.0.0" react-is "18.1.0" +react-intersection-observer@^9.8.1: + version "9.8.1" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.8.1.tgz#9c3631c0c9acd624a2af1c192318752ea73b5d91" + integrity sha512-QzOFdROX8D8MH3wE3OVKH0f3mLjKTtEN1VX/rkNuECCff+aKky0pIjulDhr3Ewqj5el/L+MhBkM3ef0Tbt+qUQ== + react-is@18.1.0: version "18.1.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -11747,7 +11836,7 @@ tiny-case@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== -tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: +tiny-invariant@^1.1.0, tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== @@ -12012,9 +12101,9 @@ typed-array-length@^1.0.4: possible-typed-array-names "^1.0.0" typescript@^5.4.3: - version "5.4.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + version "5.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== ufo@^1.3.2: version "1.4.0" @@ -12176,6 +12265,14 @@ urql@^4.0.7: "@urql/core" "^5.0.0" wonka "^6.3.2" +use-analytics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-analytics/-/use-analytics-1.1.0.tgz#ac08904989a3ecb6eff3ead95ebdb3e02e42f48c" + integrity sha512-1PMT5doiCoTm6ng9PscCHuFgl8vYNSFgGizXuFlmEBqnaRJ+KnygKnETUbBJ1W7MJPWQeHwA5Mys9t7b4P8ijw== + dependencies: + hoist-non-react-statics "^3.3.2" + tiny-invariant "^1.1.0" + use-callback-ref@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693"