LoginSignup
51
33

More than 3 years have passed since last update.

SPA(React)にGoogleAnalyticsを導入する際のパターン

Last updated at Posted at 2020-01-05

(記事にするほどではないのですが)
Reactを使ったSPAでGoogle Analyticsを使用する場合のパターンをまとめます。

準備

パターン1. react-gaを使う

メリット
  • HTMLにスニペットを追加したりしないでモジュールバンドラで完結する
  • <OutboundLink>コンポーネントが用意されているほか、多機能ではある
  • 簡潔に書ける(トラッキングIDをいちいち指定しなくてもいいなど)
デメリット
  • 最新のgtag.jsではなくanalytics.jsが使用されている
  • ファイルサイズ(17.8KB)
使い方
import ReactGA from 'react-ga';

ReactGA.initialize('トラッキングID');
ReactGA.pageview('任意のpath_name');

パターン2. gtag.js + 公式のスニペットを使う

メリット・デメリットはreact-gaを使う場合の正反対です。
公式のドキュメントにしたがってHTMLの<head>直下にコードを追加します。

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  // gtag('config', 'GA_MEASUREMENT_ID');
</script>

最後の行は通常の方法でページビューを送信しているわけですが、これからSPA特有の対応を追加しますので、コメントアウトしています。

React Routerとの組み合わせ方

通常の(昔ながらの意味でのSSRの)場合と違って考慮する必要があるのは、ページ遷移の時にページビューの送信をどう処理するかという問題です。
が、それほど難しいことではありません。分析の方針にもよると思いますので、ここでもいくつかパターンを挙げるにとどめます。

サンプルコードは全てreact-gaを使わずgtag.jsを使っています。
また、トラッキングIDはそのまま入れるように書いていますが、定数ファイルに持たせたり、create-react-appの環境変数を使うなどするのがベターだと思います。

1. history.listenでページ遷移のたびにそのままページビュー処理をする

HashRouterを使用した例です。

App.js
import React, { useRef, useEffect } from 'react';
import { HashRouter as Router, Route } from 'react-router-dom';
import Index from './containers/Index';
import Member from './containers/MemberDetail';

export default function App() {
  const router = useRef(null);

  useEffect(() => {
    router.current.history.listen((location) => {
      window.gtag('config', 'トラッキングID', {
        'page_path': location.pathname
      });
    });
  });

  return (
    <Router ref={router}>
      <div className="app-container">
        <Route path="/" exact component={Index} />
        <Route path="/members/:id" exact component={MemberDetail} />
      </div>
    </Router>
  );
}

単純な場合にはこれでも十分かもしれませんが、この場合は送信しているpathnameだけでなく、URLが少しでも変化したらリスナー関数が呼ばれページビューが送信されるので注意が必要です。
クエリパラメータなども含めて送信したい場合は

window.gtag('config', 'トラッキングID', {
  'page_path': `${location.pathname}${location.search}`
});

このようにlocation.searchを含めることで、page_path/pathname?q=fooというようにURLがセットされます。

2. ページ単位のcomponentDidMountあるいはuseEffectにページビュー処理を持たせる

どのみちタイトル(document.title)をページごとに変更する場合だったり、ページごとに臨機応変に送信するパスを変えたい(例えばURLにユーザIDが入るがアナリティクスには共通の値を送りたいなど)場合はページ単位のコンポーネントにページビューの処理を持たせます。

クラスコンポーネントの場合

Page.js
class Page extends Component {
  componentDidMount() {
    document.title = 'ページタイトル'; // ページタイトルを変更
    window.gtag('config', 'トラッキングID', {
      'page_path': '/foo' // 任意のパス(あるいはthis.props.location.pathnameなど)
    });
  }

  ...
}

FCの場合

Page.js
function Page(props) {
  useEffect(() => {
    document.title = 'ページタイトル'; // ページタイトルを変更
    window.gtag('config', 'トラッキングID', {
      'page_path': '/foo' // 任意のパス(あるいはprops.location.pathnameなど)
    });
  }, []);

  ...
}

ページごとのコンポーネントの中でも変化に応じてページビューを送信したい場合はcomponentDidUpdateを使ったりuseEffectの第二引数を変えるなどして適宜処理します。

3. ページ単位の高階コンポーネントのcomponentDidMountあるいはuseEffectにページビュー処理を持たせる

前述のページタイトルを変更する処理もそうですが、ページ単位のコンポーネントは何かしら共通する部分も出てくるはずです。
そういった場合にreact-gaのサンプルにあるように、ページを高階コンポーネントでラップするというのも有用なパターンです。
下のサンプルは、react-routerのクエリをパースする高階コンポーネントについでにページビュー処理を追加してみたものです。

createPage.js
import { createElement, useEffect } from 'react';
import PropTypes from 'prop-types';
import queryString from 'query-string';
import assign from 'lodash.assign';
import { QUERY_STRING_OPTIONS } from '../constants/common';

/**
 * react-routerのlocationからクエリを取得して扱いやすくするためのラッパーコンポーネント
 * GAページビューイベントも送信
 * @param {Component} WrappedComponent
 * @param {string} title
 * @param {string} gaPagePath
 * @returns {function}
 */
export default (WrappedComponent, title, gaPagePath) => {
  function Page(props) {
    useEffect(() => {
      document.title = title;
      window.gtag('config', 'トラッキングID', {
        'page_path': gaPagePath || props.location.pathname
      });
    }, []);

    /**
     * クエリを渡してハッシュ遷移
     * @param {string} path
     * @param {object} query
     */
    function navigateWithQuery(path = '', query) {
      const { history, location } = props;
      let qs = queryString.stringify(query, QUERY_STRING_OPTIONS);
      if (qs) {
        qs = `?${qs}`;
      }
      if (qs !== location.search) {
        history.push(`${path ? `#${path}` : location.pathname}${qs}`);
      }
    }

    const { location } = props;
    const query = queryString.parse((location.search).split('?')[1], QUERY_STRING_OPTIONS);
    return createElement(
      WrappedComponent,
      assign({}, props, { query, navigateWithQuery }),
    );
  }

  Page.propTypes = {
    location: PropTypes.shape({
      hash: PropTypes.string.isRequired,
      search: PropTypes.string.isRequired,
      pathname: PropTypes.string.isRequired,
    }).isRequired,
    history: PropTypes.shape({
      push: PropTypes.func.isRequired,
    }).isRequired,
  };

  return Page;
};

react-helmetと組み合わせる場合

react-helmetはReactのレンダリング内で(つまり、通常JSX内で)<head><title><meta>などを動的に書き換えることができるライブラリです。
あまり難しいことはなく、以下のように使うのですが、

Page.js
import React, { useEffect } from 'react';
import Helmet from 'react-helmet';

function Page(props) {
  useEffect(() => {
    console.log('title', document.title); // この時点ではまだ変更されていない
    window.gtag('config', 'トラッキングID', {
      'page_path': '/foo'
    });
  }, []);

  return (
    <div className="container">
      <Helmet>
        <title>ページタイトル</title>
        <meta name="description" content="ページの説明" />
      </Helmet>

      ...

    </div>
  );
}

微妙なことに、<Helmet>コンポーネントをレンダリングした直後、つまりこの場合componentDidMountuseEffect時にはタイトルなどは変更されていません。

Page.js
  useEffect(() => {
    console.log('title', document.title); // この時点ではまだ変更されていない
    window.setTimeout(() => {
      console.log('title', document.title); // この時点では変更済み
      window.gtag('config', 'トラッキングID', {
        'page_path': '/foo'
      });
    });
  }, []);

このようにお馴染みのsetTimeout(,0)を使ってHelmetが処理してくれるのを待ってからページビューを処理すると、タイトルが正常に反映されます。
アナリティクス.jpg

上記のサンプルコードのように、react-helmetを使う場合はページ単位のコンポーネントに副作用を持たせるわけなので、必然的にページ単位のコンポーネントでページビューの処理も一緒にすることになると思います。

[蛇足] react-helmetのあまりよくないところ

まずmetaタグで設定しているOGPはJSで動的に設定してもTwitterやFacebookには認識されない(解決したい場合はSSR)というのは周知の事実なので置いておいて、react-helmetでmetaタグを変更しようとすると、このようにあらかじめHTMLに入っていたmetaタグと重複して追加されてしまいます。

ページタイトル.jpg

微妙ですがHTMLの方にあらかじめdata-react-helmet属性を入れておくと追加されずにちゃんと更新されるようです。

<meta name="description" content="デフォルト説明" data-react-helmet="true" />
51
33
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
33