GoogleアナリティクスをReactで使う方法(パッケージ未使用)

# 概要

ReactのようなSPAの場合、GoogleAnalytics（以降、GA）は従来の要求ごとにHTMLを返すSSRがメインとなっているため、導入には工夫が必要になります。
公式ドキュメントにSPAに対応する記事がありますが、簡素なので具体的にReactでどうやって導入するか記事にしました。
https://developers.google.com/analytics/devguides/collection/gtagjs/single-page-applications?hl=ja

本記事ではReact向けのパッケージであるreact-gaを利用しません。
利用する場合は、@solt9029さんが記事を書かれていますので、そちらを参考にしてください。
https://qiita.com/solt9029/items/be0c91c3950624a8ad5e


# 読者対象

- ReactアプリでGAを導入したいが、パッケージ導入したくない。
- SEOも考慮してトラッキングを行いたい。


# Road to GA

## どうしてパッケージを導入しないのか？

パッケージを導入するかの判断基準として、独自での開発工数とバンドルサイズ増加をはかりにかけたとき、導入したほうがメリットが大きいかどうかで見ています。
今回の場合、minifyの状態でreact-gaは15KBありまして、必要最低限の独自実装なら1KB程度だろうということで未採用に至りました。
https://github.com/react-ga/react-ga/blob/master/dist/react-ga.min.js
あとSEOを考えると、ページ切り替えの際にtitleやdescriptionを変更するので、ついでに実行するだけなので実装も難しくないと判断。


## まずはGAのSPA向けページを読む

公式ドキュメント
https://developers.google.com/analytics/devguides/collection/gtagjs/single-page-applications?hl=ja
要するに、ページ切り替わったらgtag()コールすればいいだけです。
>```javascript
>gtag('config', 'GA_TRACKING_ID', {'page_path': '/new-page.html'});
>```

あと導入には当然gtag.jsが必要になるので、導入に関するドキュメントも読みます。
https://developers.google.com/analytics/devguides/collection/gtagjs/?hl=ja


一つ私がはまってしまったのが、ググると古いanalytics.jsのほうの公式ドキュメントが上位にきます。
gaってなんだ？と首をかしげることになるので、気を付けましょう。gtagが新しいほうです。
https://developers.google.com/analytics/devguides/collection/analyticsjs/single-page-applications?hl=ja


## 実際にアプリに導入する

ではまずGA導入のために、index.htmlに公式通り追加します。

```html:index.html
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_AN_CLIENT%"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  // ↓ ここから　各ページからトラッキングを送信するためグローバルに追加。
  function gtagPageview(pathname){gtag('config', '%REACT_APP_GOOGLE_AN_CLIENT%', {'page_path': pathname});}
  // ↑ ここまで　各ページからトラッキングを送信するためグローバルに追加。
  gtag('js', new Date());

  // gtag('config', 'GA_TRACKING_ID'); // ← これはSSRの場合の話で、SPAでは入れない
</script>
```

最後の行をコメントアウトします。
これをどうしてコメントアウトする必要があるのか？
それはSPAのどのページにアクセスしてもトップページにアクセスしたという、誤ったトラッキングを防止するためです。
公式ドキュメントはSSRが前提なので、返すHTMLごとにトラッキングを送信するのでこうなりますが、SPAはページの切り替わりで送信しないといけません。
コメントアウトせずに、トップページ以外のページにアクセスすると誤ったトラッキングが記録されることを確認できます。

gtagPageview()というグローバルメソッドを作り、各コンポーネントがgtagを知らずともコールできるI/Fを作っておきます。
これでコンポーネントがトラッキングする準備が整いました。

サンプルでは、GAのIDを%REACT_APP_GOOGLE_AN_CLIENT%という環境変数を作って格納しています。
環境変数について知りたい場合はCreateReactAppのドキュメントを参考にしてください。
https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables


## 開発環境でのトラッキングを防止

開発時にはトラッキングは不要というか邪魔です。
ということで、gtagPageview()をdevelop時には無効化します。

```javascript:index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Index from './pages/index';

// disable the production enviroment analytics
process.env.NODE_ENV === "development" && (window.gtagPageview = (path) => { /*console.log("pageview:", path);*/ });

ReactDOM.render(<Index />, document.getElementById('root'));
```

開発時にはprocess.env.NODE_ENVに"development"が入りますので、gtagPageview()を空メソッドにします。
(サンプルではログ出力コードをコメントアウトしています)

・・・これでOKなのですが、あとで知ったところGAには無効化するオプションがあるので、そちらのほうがよろしいかと(;^_^A

https://developers.google.com/analytics/devguides/collection/gtagjs/user-opt-out?hl=ja
>```javascript
>window['ga-disable-GA_TRACKING_ID'] = true;
>```


## 各ページのトップコンポーネントにトラッキングコードを実装

各ページのトップコンポーネントでトラッキングコードを入れていきます。

```javascript:pages/index.js

function Index(props) {
  useEffect(() => {
    document.title = `ページ内容に適したタイトル`;
    window.gtagPageview(props.location.pathname);

    // change description and rich result for SEO.
    // ...
  }, [props.location.pathname]);

  useEffect(() => {
    // page遷移後のスクロール復元
    window.scrollTo(0, 0);
  });

  return ( /* ... */ );
}
```

パスが変わったときに実行したいので、useEffectの第二引数にprops.location.pathnameを指定します。
useEffectがわからない方は、公式ドキュメントをご覧ください。
https://ja.reactjs.org/docs/hooks-effect.html

SEOに重要なtitleを変更し、gtagPageview()をコールすることでトラッキングを送信します。
後は同じくSEOに重要なdescriptionやrich resultもこのタイミングで変更します。
冒頭で述べたとおり、SEO対策のついでに送信すればいいとはこのことでした。
最低限の実装なら、パッケージ導入するまでもないのは何となく理解していただけたと思います。

ちなみに二つ目のuseEffectはページ遷移した際、スクロール位置が遷移元ページの位置を引き継がないためのコードです。
react-routerではトップレベルに一つコンポーネントを置いて対応可能としていますが、useEffect使うとこうなるかなと思って実装しました。
props.location.pathnameをトリガーとするuseEffectでは実行のタイミングが早いようで、うまく動かなかったので別になっています。
（意図しないComponentUpdateがあったとき、スクロールがトップに移動するのでバグ発見器になったりしています(;^_^A）
スクロールに関するreact-routerの公式ドキュメントはこちらです。
https://reacttraining.com/react-router/web/guides/scroll-restoration