EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

Reactで実装したコンポーネントのマウント時に工夫したこと

現在、Webページにオーバーレイで表示されるパーツを、React + Redux + react-router + redux-form + redux-thunk を使用して、コンポーネントとして実装しようと考えています。その辺りの話は機能リリース後にしたいと思いますが、そのReactで実装したコンポーネントを実際に使用するためにページにマウントする時に工夫したことについて書きたいと思います。

ページ表示時の処理を抑える

create-react-appで生成されるサンプルやReactの簡単なサンプルコードを見ていると、基本的に以下のようにページ表示時にコンポーネントをマウントするようになってます。

index.html(マウントする対象)

  <body>
    <div id="root"></div>
  </body>

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

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

このやり方だと、コンポーネントのパーツを使わないユーザーでもページの表示時にReactのロード処理が実行されてしまいます。それを避けページ表示時の処理を減らすように、以下のようにパーツ表示(ボタンクリック)時にコンポーネントをマウントするようにしてみました。

index.html(マウントする対象)

  <body>
    <div id="root"></div>
    <a href="javascript:void(0);" id="entry">表示</a>
  </body>

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';


document.getElementById('entry').addEventListener('click', e => {
  const target = document.getElementById('root');
  const closeHandler = e => {
    ReactDOM.unmountComponentAtNode(target); // オーバーレイ閉じる用
  }
  ReactDOM.render(<App closeHandler={closeHandler}/>, target);
});

以上により、ページ表示時にReactのロード処理を回避することができると思います。

マウント時に初期パラメータを渡す

パーツを表示する時に区分やマスタの値、ログインユーザーの情報などを初期のパラメータとして渡したいと思いました。Reactのコンポーネントがマウントされた時にAPIを使用してサーバーから初期値を取得することもできますが、せっかく表示元ページをJSPで動的に出力しているので、余計な通信をしないでページに埋め込まれた値(JSON)をコンポーネントに渡すようにしてみました。

index.html(マウントする対象)

  <body>
    <div id="root"></div>
    <span id="initParam" style="display:none;">
      {"title":"フォームタイトル", "body":"フォーム説明文"}
    </span>
    <a href="javascript:void(0);" id="entry">表示</a>
  </body>

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';


document.getElementById('entry').addEventListener('click', e => {
  const initParam = document.getElementById('initParam').innerHTML;
  const target = document.getElementById('root');
  const closeHandler = e => {
    ReactDOM.unmountComponentAtNode(target); // オーバーレイ閉じる用
  }
  ReactDOM.render(
    <App param={initParam} closeHandler={closeHandler}/>, 
    target
  );
});

App.js

import React, { Component } from 'react';
import { Modal } from 'react-overlays';

class App extends Component {
  render() {
    const {closeHandler,param} = this.props;
    const initParam = JSON.parse(param); // 初期値取得
    return (
      <div className="assessment">
        <Modal
          onHide={closeHandler}
          show={true}
          className="modal"
        >
          <div className="dialog">
            <a onClick={closeHandler}>閉じる</a>
            <h1>{initParam.title}</h1>
            <p>{initParam.body}</p>
            本体
          </div>
        </Modal>
      </div>
    );
  }
}

以上で、呼び出し元のHTMLで設定されているタイトルや本文が表示されると思います。

Code Splittingをして読み込み量を抑える

Reactのソースをnpm run buildでビルドするとwebpackでjsファイルにパッキングされますが、Code Splittingをすることで一度に全てのスクリプトを読み込まずに、必要なタイミングでロードすることができるようになります。この仕組みを使うと、ページ表示時に取得するスクリプトのサイズを抑えることができるので、初期表示を速くすることができると思います。

Code Splittingを実現するためにはdynamic importという仕組みを使うのですが、今回はloadable-componentsというライブラリを使用してimportすることで実現しました。

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import loadable from 'loadable-components'

const App = loadable(() => import('./App')); // importの仕方を変更

document.getElementById('entry').addEventListener('click', e => {
  const initParam = document.getElementById('initParam').innerHTML;
  const target = document.getElementById('root');
  const closeHandler = e => {
    ReactDOM.unmountComponentAtNode(target); // オーバーレイ閉じる用
  }
  ReactDOM.render(
    <App param={initParam} closeHandler={closeHandler}/>, 
    target
  );
});

この対応により、テストコードでもページ表示時の読み込み量が2/3程度になりました。

f:id:eatsmart:20190425163102p:plain
Code Splitting前

f:id:eatsmart:20190425162945p:plain
Code Splitting後

Code SplittingはRouteごとに分割するのが良さそうで、実は今回はAppの中身はreact-routerでルーティングするので、App.jsにも使ってみました。

App.js

import React, { Component } from 'react';
import { Modal } from 'react-overlays';
import {Route, Switch } from 'react-router-dom'
import loadable from 'loadable-components'

class App extends Component {
  render() {
    const {closeHandler,param} = this.props;
    const initParam = JSON.parse(param);
    return (
      <div className="assessment">
        <Modal
          onHide={closeHandler}
          show={true}
          className="modal"
        >
          <div className="dialog">
            <a onClick={closeHandler}>閉じる</a>
            <h1>{initParam.title}</h1>
            <p>{initParam.body}</p>
            <Route component={AppRoute} />
          </div>
        </Modal>
      </div>
    );
  }
}

const Main = loadable(() => import('./Main'));
const Product = loadable(() => import('./product/Product'));
const Comment = loadable(() => import('./comment/Comment'));

const AppRoute = (props) => (
  <Switch>
    <Route path="/product" component={Product} />
    <Route path="/comment" component={Comment} />
    <Route component={Main} />
  </Switch>
)

export default App;

この対応で、コンポーネント内のページも必要に応じてスクリプトがロードされるようになると思います。

最後に

今回、Webページ上のオーバーレイ表示の中で、ページ遷移を含むSPA的なコンポーネントを実装するためにReactを検討しました。 Reactを使用することで、直接DOMを意識することなくUIを実装することができ、開発は楽になりそうでしたが、実際のページ表示やパフォーマンスが耐えうるものか不安がありました。

この記事のような工夫をすることで、実運用に採用できそうだなと思っています。