# Implementing GA4 in your React project

In this guide, we’ll walk you through a more advanced GA4 implementation to your React.js project that's both easily extendable and testable. Additionally, we'll highlight common obstacles faced during this integration process and provide straightforward solutions. Let’s dive in.

- URL: https://sudolabs.com/insights/implementing-ga4-in-your-react-project
- Published: 2024-01-09
- Updated: 2024-02-01
- Author: Kristián Müller
- Categories: Tech
- Language: English

Our exploration centers around a comprehensive solution that involves the creation of a versatile provider capable of forking instances. Each instance not only manages its own data storage but also facilitates streamlined parsing of essential information, like lists of items, into the revered **dataLayer**. As we navigate through this solution, we'll uncover practical implementations, insightful transformations, and an array of techniques that empower React developers to conquer the complexities of data management.

## Problem

We should be able to store data from the parent component independently from the child component. This means that if we have a child component as a widget and we need to execute an event to GA4, it should be able to read data from its parent, for example: the list of products, page area, etc. We should also consider the fact that widgets can be used in multiple places at once, so they should be able to access all necessary data from the parent component directly themselves.

## Solution

- Creating a provider that we can fork, respectively create a new instance of it in the React tree {if needed}.
- Every instance should be able to store data
- Instances can hold data like the list of items that we need to parse into the `dataLayer`

Visual representation of the described solution above:

![](https://a.storyblok.com/f/117609/1116x628/52ae2ab2f4/visual-interpratation.png)

## Higher Order Component for shared properties

Since there may be a necessity for shared properties like `area`, `page_area`, etc. The best way to handle this is to create a Higher Order Component {HOC} which we can use on specific routes/pages .{for example `/shop`}

This way we can store properties in the provider for event execution calls in the React tree below the wrapped component.

After some event execution call happens, its transformer can then easily read those property values from the provider tree with the helper function `findPropertyInTree`.

Example Higher Order Component:

```typescript
import { ComponentType, VoidFunctionComponent } from 'react'
import GoogleAnalyticsProvider, {
  useGoogleAnalytics,
} from '~/GoogleAnalyticsProvider'

interface ProviderDataProps {
  area: string
  page_area: string
  item_list_name: string
	...OTHER_PROVIDER_DATA_PROPS
}

export const withGA4Provider =
  (providerDataProps: Partial<ProviderDataProps>) =>
  (Component: ComponentType<object> | VoidFunctionComponent<object>) => {
    const WrappedComponent = <T extends object>(props: T) => {
      const GA4 = useGoogleAnalytics()

      return (
        <GoogleAnalyticsProvider parent={GA4} data={providerDataProps}>
          <Component {...props} />
        </GoogleAnalyticsProvider>
      )
    }

    return WrappedComponent
  }
```

Example usage:

```typescript
export const Shop = withGA4Provider({
  area: '...',
  page_area: '...',
  item_list_name: '...',
	...OTHER_PROVIDER_DATA_PROPS,
})(ShopPageComponent)
```

## executeDataToDataLayer function

After creating `GoogleAnalyticsProvider` we need to define a common method that will be used for calling events.

The method should allow us to easily pass event name, event-specific raw data, and a boolean flag on whether to include aggregated data that we are storing in the provider. {these data may include for example: items that the user has viewed on some page on our website → we should be aggregating these data with some observer}

After the function gets called it should call the correct transformation depending on the `event` prop that is being passed, to transform the data that are passed as well.

Lastly, the method should either have conditions in itself or call another method where the event to dataLayer pushing strategy is implemented.

Example executeDataToDataLayer function:

```typescript
const executeDataToDataLayer = useCallback(
    ({
      event,
      overrideData = {},
      includeStoredData = true,
    }: ExecuteDataToDataLayerProps<typeof overrideData>) => {
      // If google analytics are not loaded yet, it should not process this method.
      if (
        !window?.dataLayer ||
        process.env.NEXT_PUBLIC_NODE_ENV !== 'production'
      ) {
        return
      }

      // Context data represent data of current provider overrided by overrideData
      const contextData = {
        ...(includeStoredData && storedData.current),
        ...overrideData,
      }

      if (GA4_ECOMMERCE_EVENTS[event]) {
        if (isPageViewLoading) {
          dataLayerBuffer.current.push({ ecommerce: null })
        } else {
          window.dataLayer.push({ ecommerce: null })
        }
      }

			// In SPA eccomerce events require _clear parameter
      const isClearEvent = GA4_ECOMMERCE_EVENTS[event]

      try {
				// Transform passed data for the event that is being called
        const transformedInputData = transformationInputDataToGA4DataLayer({
          event,
          contextData,
          parent,
          ...OTHER_COMMON_PROPS // For example: pathname, user, userType, etc.
        })

        if (!transformedInputData) {
          return
        }

				// Call function with the dataLayer pushing strategy
				// -> Takes care of the order how the events are being pushed to dataLayer
        handleDataLayerPushMethod({ event, isClearEvent, transformedInputData })
      } catch (err) {
        const errorMessage = err instanceof Error ? err.message : err
        const fullError = new Error(
          JSON.stringify({
            error: errorMessage,
            event,
            contextData,
          })
        )
        Sentry.captureException(fullError)
      }
    },
    [
      dataLayerBuffer,
      handleDataLayerPushMethod,
      isPageViewLoading,
      parent,
			...OTHER_COMMON_PROPS,
    ]
  )
```

## Transformations

What are transformations?

- In simple terms, `transformators` are functions which take raw data as an input
- These functions transform then the passed data and return them in a form they must be pushed into the `dataLayer`

Why transformations?

- Can be tested as a unit
- Are easy to extend
- Offer a great error-handling approach

### Writing transformation

Helper function for accessing properties from parent providers:

```typescript
export const findPropertyInTree = <T = any>({
  keyName,
  contextData,
  parent,
}: FindPropertyInTreeProps): T => {
  const prop = get(contextData, keyName)

  // Check type of prop instead of value (0 can be valid value)
  if (prop !== undefined) {
    return prop
  }

  if (!parent) {
    return undefined
  }

  return findPropertyInTree({
    keyName,
    contextData: parent.data.current,
    parent: parent.parent,
  })
}
```

Example transformation for click event:

```typescript
import { GA4_PARAMS, TransformationProps } from '~/types'
import { findPropertyInTree } from '~/utils'

// Interface of context data passed to the transformation
interface ClickTransformationContextData {
  text: string
  url?: string
  name?: string
}

type ClickTransformationProps = TransformationProps<
  ClickTransformationContextData, // contextData type
  'contextData' | 'parent' // Parameters we want to pick from general provider type that may be extended with some others
>;

export const ExampleTransformation = ({
  contextData,
  parent,
}: ExampleTransformationProps) => {
  const { text, url = '', name = '' } = contextData

  return {
    exampleParamZero: findPropertyInTree({
      keyName: GA4_PARAMS.PAGE_AREA, // For parameter name, we can use a common declared constant that holds all of our parameters {e.g. GA4_PARAMS}
      contextData,
      parent,
    }),
    text,
    url,
    name,
  }
}
```

### Testing transformation

Example test for click event transformation:

```typescript
import { ClickTransformation } from '~/transformations'

describe('GA4 - click', () => {
  it('should transform correctly', () => {
    const result = ClickTransformation({
      contextData: {
        text: 'text',
        url: 'url',
        name: 'name',
      },
    })

    expect(result).toMatchObject({
      text: 'text',
      url: 'url',
      name: 'name',
    })
  })
})
```

## Tracking page view event on Single Page Applications

How to track page view events on Single Page Application {SPA}?

- The best approach to go when tracking page\_view event on an SPA is to create a wrapper component that should take just children as props if there isn’t a necessity for any other props to be passed.
- This component should be placed on the outermost level of the component tree in `_app.tsx` .
- By going with this approach we ensure that the page\_view event is being called on every page. But what is more, we can also add a route restriction mapping so we ensure that the event is not being executed on some specific pages.

Example of the wrapper component:

```typescript
import React, { useEffect } from 'react'

interface Props {
	children: React.ReactNode
}

export const PageViewTracker: React.FC<Props> = ({ children }) => {

	{/* ... */}

  useEffect(() => {
    if (
      YOUR_CONDITIONS
    ) {
      PAGE_VIEW_EVENT_CALL
    }
  }, [YOUR_DEPENDENCIES])

	{/* ... */}

  return <>{children}</>
}
```

### Accessing title from window object

How can we access the page title?

- When sticking with the approach described above there shouldn’t be any problem with accessing the title directly from the window object, since the window object is available in `useEffect` hook.

## Event order

Google Analytics has prescribed the order in which events must be pushed to `dataLayer`, some events may vary depending on the specific project.

The following order applies for each SPA:

1. `consent_default` ⇒ region-specific default cookie consent settings
2. `consent_update` ⇒ user’s consent settings
3. `page_view`
4. …

These events should be fired on each page in this order.

### useEffect hook problem

Since many event calls must be called in `useEffect` hook and the fact that Google Analytics has a prescribed order in which some of the events must be pushed into the `dataLayer`, we must understand how `React.js` handles their execution:

During the execution of the parent component, if a child component needs to be rendered, the parent component’s render is halted. The child component starts rendering and after its (child component’s) rendering is complete, its (child component’s) set of `useEffect` hooks are processed. After it’s done, the parent component’s render is resumed.

Refer to below for a visual representation of the explanation above.

Sample code:

```typescript
import React, { useEffect } from 'react'

const Child: React.FC = () => {
	console.debug('Child.render: start')

	useEffect(() => {
		console.debug('Child.useEffect')
	}, [])
	
	console.debug('Child.render: end')
	return <p>Child</p>
}

const Parent: React.FC = () => {
	console.debug('Parent.render: start')

	useEffect(() => {
		console.debug('Parent.useEffect')
	}, [])
	
	console.debug('Parent.render: end')
	return <Child />
}

export default Parent
```

Console output:

```plaintext
🐞 Parent.render: start
🐞 Parent.render: end
🐞 Child.render: start
🐞 Child.render: end
🐞 Child.useEffect
🐞 Parent.useEffect
```

In navigating our project's expansion, integrating Google Analytics into our website emerges as a pivotal step. While our highlighted Google Analytics implementation in React.js provides a solid foundation, it's essential to recognize that it may not be the exclusive solution for every project. The beauty of this integration lies in its versatility – effortlessly extendable, easily testable, and offering the flexibility to distribute event execution seamlessly throughout your codebase.

This tool not only empowers us to make data-driven decisions by providing insights into user behavior and engagement but also ensures that we stay agile and responsive to evolving trends with its real-time analytics capabilities. As we delve into this integration, it becomes clear that it's not merely about accommodating growth; it signifies a commitment to leveraging actionable data for the continued success of our project.
