みなさん、ReactでGTMをどう設定していますか？悪戦苦闘した試行錯誤の末に。

# TL;DR
ReactでGTMを利用するなら、それぞれのトリガーについて以下の設定をする必要があります。

1. [**要素の表示トリガー**：ページ遷移をGTMにアピールする](#1-要素の表示トリガーページ遷移をgtmにアピールする)
1. [**クリックのトリガー**：リンク以外のものをトリガーするなら変数にカスタムJavaScriptを設定する](#2-クリックのトリガーリンク以外のものをトリガーするなら変数にカスタムjavascriptを設定する)
1. [**スクロールのトリガー**：ページ遷移をGTMに通知し、拡張イベント機能計測と重複しないように気を付ける](#3-スクロールのトリガーページ遷移をgtmに通知し拡張イベント機能計測と重複しないように気を付ける)

# 背景
Google Analytics 4 (GA4)で要素の表示やクリック、スクロールを計測しています。計測対象のWebサイトは、Gatsby.jsで開発しており、GA4への接続はGoogle Tag Manager (GTM)を利用しています。GTMの標準的なトリガーは、SPAであるReactでは期待したように動かないことが多々あります。それらの原因を探り、様々な試行錯誤の末に辿り着いた方法をまとめます。

今回の実装ではトリガーの設定はGTMで、トリガーの対象となる要素の指定はコードで管理するという方法をとっています。具体的には以下の記事と同様の実装です。このような実装としたのは、GTM周りの管理を開発者が担当しているためで、非開発者が管理する必要がある場合には追加の設定や実装を検討する必要があると思います。

https://and-ha.com/web-technical-info/gtmga4-customdata/

# 実装
## 1. 要素の表示トリガー：ページ遷移をGTMにアピールする
GTMでは、SPAを利用していると、期待通りに発火しないことがあります。そしてその解決方法が一般的に確立されていないということまで含めて、あるあるだと思います。


当初はGTMで設定されている[トリガー](https://support.google.com/tagmanager/answer/7679410?hl=ja)を利用して試行錯誤していました（[後述](#当初利用していた方法)します）が、どんなに頑張っても解決に至らなかったため、以下の記事と同様にIntersection Observerを利用しました。

https://qiita.com/cheez921/items/a9e8d257684098db55c3#%E8%A6%81%E7%B4%A0%E3%81%AE%E8%A1%A8%E7%A4%BA

上記の記事を参考に、[react-intersection-observer](https://www.npmjs.com/package/react-intersection-observer)を利用した上で、同じ設定を使い回しやすいよう、カスタムフックにしてみました。

```typescript:useElementVisibility.ts
import { useInView } from 'react-intersection-observer'

export const useElementVisibility = (viewedComponent: string) => {
  return useInView({
    threshold: 0.5,
    triggerOnce: true, // GTMの、要素の表示トリガーと同様にページごとに1回発火させたい
    onChange(inView) {
      if (inView) {
        window.dataLayer = window.dataLayer || []
        window.dataLayer.push({
          event: 'viewComponent',
          viewedComponent,
        })
      }
    },
  })
}
```

計測対象のコンポーネントを呼び出す場所で、以下のように設定します。

```typescript
import { useElementVisibility } from '../../models/ElementVisibility'
...
  const [targetRef] = useElementVisibility('計測対象として管理したい変数を入れる')

// `key`の属性は、子コンポーネントの一部のみ切り替わるようなページ遷移の場合に必要となります。続けて詳しい説明をします。
...

            <div key={pageIndex}>
                <div ref={targetRef}>
                  {/* 計測対象のコンポーネント */}
                </div>
            </div>

...
```

ここで1つ気をつけるべきポイントがあります。それはページの一部のみを切り替えるような遷移の場合です。例えばニュースサイトの場合、複数のページから成る記事の、記事内における遷移です。（タイトルや目次などの構造の大部分は切り替わらず内側のコンテンツ部分が切り替わるイメージ、クエリパラメータで[`?page=2`](https://fashiontechnews.zozo.com/research/workman_fusiondown?page=2)みたいなイメージです。）

この場合、切り替わらなかったコンポーネントは既にトリガーが発火しており、ページ遷移（クエリパラメータが変わっても）しても再度発火しません。これを解決するには、要素の表示を計測するコンポーネントより親の、ページ切り替えを担ってるコンポーネントに、ページごとに異なる値をkeyに渡す必要があります。こうすることでページ遷移と共に配下のReactコンポーネントが別のものとして描画され、Intersection Observerがページ遷移後でも発火するようになります。

### 当初利用していた方法
当初はライブラリを入れなくてもいい以下の方法で運用していたのですが、上記の問題のようにページの一部のコンポーネントのみを切り替えるページ遷移が必要となりました。ライブラリを増やさないようにと試行錯誤したのですが、Safariでどうしても解決できず上記の結論に至りました。
<details><summary>コンポーネントの一部を切り替える遷移が無いならこちらの方法も使えます</summary>

概ね[こちらの記事](https://ayudante.jp/column/2023-07-18/12-00/
)と同様に、計測対象にdata属性で設定します。

しかしながら、同じ構造を持つページ間、つまり同じReactのテンプレートコンポーネントを利用しているなどの場合に、なぜかページ遷移が検知されません。

解決策は、トリガー対象となるdata属性の値をマウント後に更新することです。今回要素の表示トリガーの対象として設定したのが`data-gtm-view`で、この属性がついていれば発火します。`gtmView`という変数に計測対象として記録したい文字列を渡し、マウント後にuseEffect内で設定するようにします。こうすることでページ遷移する度に`data-gtm-view`の値が更新していると認識され、発火するようになります。

```tsx
...

  const [gtmView, setGtmView] = useState<string>()

  useEffect(() => {
    setGtmView('toppage-latest-container')
  }, [])

...

        <div data-gtm-view={gtmView}>

...
```
</details>


## 2. クリックのトリガー：リンク以外のものをトリガーするなら変数にカスタムJavaScriptを設定する
クリックトリガーは[GTM標準のトリガー](https://support.google.com/tagmanager/answer/7679320?hl=ja)を使います。その場合`全ての要素`か`リンクのみ`のどちらかを選択できます。`リンクのみ`を選ぶメリットとしては、確実にページ遷移が行われるaタグのみを指定できることです。

`全ての要素`を利用すると、ページ遷移しないボタンなどの任意の要素のクリックでトリガーすることができます。しかしトリガーされた要素の値を取得して保存したい場合に問題が発生します。例えば`<button>`の要素にdata属性で値を設定していたとします。もし`<button>`の子要素で、クリックのトリガーが発生しても、通常では親要素である`<button>`の値にアクセスできません。

解決策はまさにこちらにまとめてくださっているので、ご紹介します。

https://qiita.com/hamakou108/items/3bc5b1f4db3ec23945e6

```tsx:計測対象のコンポーネント
...

              <div
                data-gtm-click="計測対象として管理したい変数を入れる"
                data-gtm-id={articleId}
                data-gtm-order={index + 1}
              >

...
```


```javascript:GTMの変数に設定するカスタムJavaScript
function() {
  var attribute = 'data-gtm-click'
  return {{Click Element}}.closest('[' + attribute + ']').getAttribute(attribute)
}
```

## 3. スクロールのトリガー：ページ遷移をGTMに通知し、拡張イベント機能計測と重複しないように気を付ける
スクロールも[GTM標準のトリガー](https://support.google.com/tagmanager/answer/7679218?hl=ja)を利用しますが、計測する上で気を付けるべきポイントは以下です。

a. SPAでページ遷移していることを認識させるために、履歴の変更をトリガーに、gtm.loadを実行する
b. 重複しないように[拡張イベント計測機能](https://support.google.com/analytics/answer/9216061?hl=ja)のスクロールをオフにすることを忘れない

aのコードは以下になります。ページ遷移時、今回は`履歴の変更`トリガー時に以下を実行するようにしました。カスタムHTMLとしてタグに登録する。
```html
<script>
  window.dataLayer = window.dataLayer || [];
  setTimeout(function() {
      window.dataLayer.push({
        'event': 'gtm.load'
      });
  }, 500); // ページ全体が読み込まれるまで待つ
</script>
```

# 感想
大変でした。みなさんどうやって計測しているだろうって思ってます。
もっといいツールないかなー。無いなら作るしかないのかなー。