目指せ!JavaScript カスタマイズ中級者(6)〜React 編〜

目次

はじめに〜昨今のライブラリ事情〜

フロントエンド界隈のトレンドは流れが速く、キャッチアップしづらい状況でしたが、最近は比較的安定しています。
JavaScript のフレームワークやライブラリにも栄枯盛衰がありますが、近ごろは React や Vue.js などが多くの開発者に利用されています。
ライトな開発ならそのようなライブラリを導入せずとも JavaScript カスタマイズする上では問題はありませんが、ある程度規模が大きくなると、ライブラリに頼ったほうが効率的に開発できます。

次の記事では Vue.js と kintone の組み合わせの良さを紹介しました。
kintone と Vue.js は相性がいい?Vue.js で独自のビューと検索フォームを作ってみよう

今回は React を使って kintone JavaScript カスタマイズに導入した場合のイメージを確認し、プロジェクトにフィットするものを選ぶための材料にしてもらえればと思います。

目指せ中級者シリーズの記事一覧は以下を参照してください。
目指せ中級者シリーズ

React とは:React の特徴

React は、Facebook と React のコミュニティによって開発されているユーザインターフェース構築のための JavaScript ライブラリです。
ここでは、 React の公式サイト (External link) に書かれている、3 つの特徴を解説します。

宣言的な View

「宣言的」というのは、条件やどのように動作するかなどが、明確に示されている状態です。
たとえば jQuery は「手続き的」で、DOM 操作なら要素を取得しその要素に対し命令を積み上げていきます。
コードのいろんな箇所から要素に対して命令をできるので、複雑に絡み合うとバグの温床になります。

たとえば、「クリックしたとき、交互にその要素の色を青または赤に変更する」というものを書いてみます。

  • jQuery

    1
    2
    3
    4
    5
    6
    7
    8
    
    const element = $('#target');
    element.on('click', () => {
      if (this.backgroundColor === 'red') {
        this.backgroundColor = 'blue';
      } else {
        this.backgroundColor = 'red';
      }
    });
  • React

    1
    2
    3
    4
    5
    
    const Element = () => {
      const [color, setColor] = useState('red');
      const clickHandler = () => (color === 'red' ? setColor('blue') : setColor('red'));
      return <div style={{backgroundColor: color}} onClick={clickHandler} />;
    };

これだけですと、大した差はないかもしれませんが、jQuery の場合は、どこからでもイベントハンドラーを追加できてしまいます。
また、「今の要素の色は何色か?」というような状態も DOM で管理することになる(要素から現在の色を読み取る)ので、コード上でやることが増えていった場合、どんどん複雑になり、正常に動作させ続けることが難しくなります。
React の場合は、DOM と状態を分けて管理するため、状態が自明です。

コンポーネントベース

React は、自分自身の状態を管理するカプセル化されたコンポーネントを作成し組み合わせることで、複雑なユーザインターフェースを構築できます。

コンポーネントのロジックは、Template ではなく JavaScript そのもので書くことができるので、さまざまなデータをアプリケーション内で簡単に取り回すことができ、かつ DOM に状態を持たせないようにできます。

上記サンプルコードも、「赤または青」という情報は、color 変数が保持しており、DOM の状態を意識する必要がありません。
また、その状態管理やイベントハンドリングもひとつにまとめたコンポーネントとできます。

一度学習すれば、どこでも使える

React と組み合わせて使用する技術に制限はありません。
React を使って新しい機能を追加する際に、既存のソースコードを書き換える必要はありません。

React はサーバー上でもレンダー(サーバーサイドレンダリング)できますし、React Native を使うことでモバイルアプリケーションの中でも動きますので、一度覚えると流用が可能です。

React を導入する際の検討ポイント

React には次のようなメリットとデメリットがありますので、導入の際は、これらを加味して検討するとよいでしょう。

メリット

宣言的に書ける

jQuery やプレーンな JavaScript と比較すると、宣言的に書けることが一番の強みです。
定義されたコンポーネントは、コンポーネントの状態が変化すると、コードの上から下まで都度実行されます。
上述のサンプルコードだと、useState で宣言された color 変数の値が状態にあたります。
これにより、何が起きているかが明確になります。
たとえば jQuery やプレーンな JavaScript だと、DOM に対してどこからどの命令が起きるかはわからず、複雑性が増していき、コントロールがたいへんになっていきます。

一覧のカスタマイズビューの実装や、独自のボタン・要素を数多く扱いたいときなどに有効です。

ブラックボックス的な部分が少ない

Vue.js やその他ライブラリと比較したとき、React は特別なルールが少なく(Vue.js でいう v-model ディレクティブなど)、何が行われているかがわかりやすいです。
サンプルコードのとおり、イベントハンドリングや値がどうなるかというのはプレーンな JavaScript を書く感覚と同じで、すべて自分で指定できます。

人気があり学習コンテンツやコンポーネントが豊富

React は JavaScript ライブラリの中ではかなりの人気があり、学習しやすくいろんなコンポーネントも公開されています。
上述のとおりいろんなところに流用可能です。
作り方にもよりますが、kintone JavaScript カスタマイズ上で定義していたコンポーネントを、そのままモバイルアプリの作成にも流用できます。

部分的に導入できる

一部の JavaScript カスタマイズはプレーンな JavaScript にして、複雑な箇所から React を適用するといったことも可能です。

UI とロジックを分離できる

UI とロジックが分離できるので、大規模な開発にも向いています。
特に、いろんなアプリから REST API でデータを取得して加工する、などのロジック部分を UI から分けられるので、ロジックを書く箇所がすっきりします。

デメリット

記述量が増えやすい

宣言的にかけるがゆえに、記述量としては増える傾向にあります。
数行で済むようなちょっとした JavaScript カスタマイズならプレーンな JavaScript でこと足ります。
条件によってテキストの色をかえる、などの JavaScript カスタマイズなどでは、あえて React を導入する必要はありません。

どちらかというと、ずっと面倒を見ていく kintone 環境などに向いていると思います。

パフォーマンスを考慮する場合は考慮が必要

パフォーマンスを向上させるためにはメモ化をする必要があるなど、場合によっては多少難易度があがります。

カスタマイズビューを React で構築する

今回は、 顧客リストアプリ (External link) を利用し、カスタマイズビューを作成するサンプルコードを作ります。

会社名が一覧として表示され、チェックできます。
ボタンを押すと、現在選択されている会社名を表示できます。

下準備

  1. 顧客リストアプリ (External link) を追加します。

  2. カスタマイズビューを設定し、次の HTML を指定してください。

    1
    
    <div id="target"></div>

  3. Github から以下のリポジトリをクローンしてコードを用意します。
    https://github.com/cybozudevnet/sample-kintone-webpack-for-intermediate (External link)

詳しくは 第 1 回 の記事からためしてみてください。
React を試せるような設定になっています。

サンプルコード

上記を実現するための React を使ったサンプルコードは次のようになります。
リポジトリのファイルは「src/apps/react-sample/index.tsx」です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React, {useState} from 'react';
import ReactDOM from 'react-dom';

// Componentの定義
const ChecklistComponent: React.FC<{records: KintoneTypes.SavedCustomer[]}> = ({records}) => {
  // チェックしたIDを保管する
  const [selectedIds, setSelectedIds] = useState<string[]>([]);

  // ボタンを押したときのハンドラ
  const buttonHandler = () => {
    if (selectedIds.length === 0) {
      alert('何も選択されていません。');
      return;
    }
    // なにか選択されていれば、会社名を表示する。
    alert(`${selectedIds.map((id) => records.find((r) => r.$id.value === id)?.会社名.value).join('\n')}`);
  };

  // チェックボックスを押したときのハンドラ
  const checkboxHandler = (recordId: string) => (e:React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.checked) {
      // チェックされた場合、チェックしたIDを含めて新しい配列を返却
      setSelectedIds((current) => [...current, recordId]);
    } else {
      // チェックを外された場合、チェックしたIDを消す
      setSelectedIds((current) => {
        // すでに選ばれているか念の為確認
        const targetIndex = current.indexOf(recordId);
        if (targetIndex === -1) return current;
        // 該当のindexを切り取って新しい配列を返却
        return [...current.slice(0, targetIndex), ...current.slice(targetIndex + 1)];
      });
    }
  };

  // 要素の定義と返却
  return (
    <div style={{margin: '8px 16px'}}>
      <div>
        <button onClick={buttonHandler}>選択されている顧客を表示する</button>
      </div>
      {records.map((record) => {
        return (
          <div style={{margin: '4px 8px'}} key={record.$id.value}>
            <label>
              <input type="checkbox" onChange={checkboxHandler(record.$id.value)} checked={selectedIds.includes(record.$id.value)} />
              <span style={{paddingLeft: '4px'}}>{record.会社名.value}</span>
            </label>
          </div>
        );
      })}
    </div>
  );
};

kintone.events.on('app.record.index.show', async (event) => {
  const records = event.records as KintoneTypes.SavedCustomer[];

  const targetEl = document.querySelector('#target');
  if (targetEl == null) return;
  // Componentを描画
  ReactDOM.render(<ChecklistComponent records={records} />, targetEl);
});

コードの概要説明

大まかに「コンポーネントの定義」と「コンポーネントの表示」に分けて説明します。 今回は、前回説明した TypeScript で利用したコードになります。

コンポーネントの定義

今回は、複数のチェックボックスをひとつのコンポーネントとして定義しました。

 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Componentの定義
const ChecklistComponent: React.FC<{records: KintoneTypes.SavedCustomer[]}> = ({records}) => {
  // チェックしたIDを保管する
  const [selectedIds, setSelectedIds] = useState<string[]>([]);

  // ボタンを押したときのハンドラ
  const buttonHandler = () => {
    if (selectedIds.length === 0) {
      alert('何も選択されていません。');
      return;
    }
    // なにか選択されていれば、会社名を表示する。
    alert(`${selectedIds.map((id) => records.find((r) => r.$id.value === id)?.会社名.value).join('\n')}`);
  };

  // チェックボックスを押したときのハンドラ
  const checkboxHandler = (recordId: string) => (e:React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.checked) {
      // チェックされた場合、チェックしたIDを含めて新しい配列を返却
      setSelectedIds((current) => [...current, recordId]);
    } else {
      // チェックを外された場合、チェックしたIDを消す
      setSelectedIds((current) => {
        // すでに選ばれているか念の為確認
        const targetIndex = current.indexOf(recordId);
        if (targetIndex === -1) return current;
        // 該当のindexを切り取って新しい配列を返却
        return [...current.slice(0, targetIndex), ...current.slice(targetIndex + 1)];
      });
    }
  };

  // 要素の定義と返却
  return (
    <div style={{margin: '8px 16px'}}>
      <div>
        <button onClick={buttonHandler}>選択されている顧客を表示する</button>
      </div>
      {records.map((record) => {
        return (
          <div style={{margin: '4px 8px'}} key={record.$id.value}>
            <label>
              <input type="checkbox" onChange={checkboxHandler(record.$id.value)} checked={selectedIds.includes(record.$id.value)} />
              <span style={{paddingLeft: '4px'}}>{record.会社名.value}</span>
            </label>
          </div>
        );
      })}
    </div>
  );
};

React では JSX (External link) を利用して、要素の定義ができます。
36〜53 行目のように、HTML を書くような感覚でかけます。

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 要素の定義と返却
return (
  <div style={{margin: '8px 16px'}}>
    <div>
      <button onClick={buttonHandler}>選択されている顧客を表示する</button>
    </div>
    {records.map((record) => {
      return (
        <div style={{margin: '4px 8px'}} key={record.$id.value}>
          <label>
            <input type="checkbox" onChange={checkboxHandler(record.$id.value)} checked={selectedIds.includes(record.$id.value)} />
            <span style={{paddingLeft: '4px'}}>{record.会社名.value}</span>
          </label>
        </div>
      );
    })}
  </div>
);

ボタンを押したときのハンドラーや、チェックボックスをおしたときのハンドラーは、それぞれ 10〜17 行目、20〜34 行目のように宣言します。

10
11
12
13
14
15
16
17
const buttonHandler = () => {
  if (selectedIds.length === 0) {
    alert('何も選択されていません。');
    return;
  }
  // なにか選択されていれば、会社名を表示する。
  alert(`${selectedIds.map((id) => records.find((r) => r.$id.value === id)?.会社名.value).join('\n')}`);
};
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const checkboxHandler = (recordId: string) => (e:React.ChangeEvent<HTMLInputElement>) => {
  if (e.target.checked) {
    // チェックされた場合、チェックしたIDを含めて新しい配列を返却
    setSelectedIds((current) => [...current, recordId]);
  } else {
    // チェックを外された場合、チェックしたIDを消す
    setSelectedIds((current) => {
      // すでに選ばれているか念の為確認
      const targetIndex = current.indexOf(recordId);
      if (targetIndex === -1) return current;
      // 該当のindexを切り取って新しい配列を返却
      return [...current.slice(0, targetIndex), ...current.slice(targetIndex + 1)];
    });
  }
};

また、7 行目にチェックボックスでチェックした ID を保管するようの State(状態)を定義しています。
useState を使うことで状態を保持できます。

7
const [selectedIds, setSelectedIds] = useState<string[]>([]);

この内、selectedIds は今保持している ID がはいっており、setSelectedIds で ID のセットができます。

このように、要素の定義と、そのハンドラーの定義、状態をセットで書くことができ、ひとつのコンポーネントとして宣言できます。
jQuery などでは状態を class などで表現したりしますが、React では UI と切り分けて状態を保持できるのも大きな利点です。

コンポーネントの表示
56
57
58
59
60
61
62
63
kintone.events.on('app.record.index.show', async (event) => {
  const records = event.records as KintoneTypes.SavedCustomer[];

  const targetEl = document.querySelector('#target');
  if (targetEl == null) return;
  // Componentを描画
  ReactDOM.render(<ChecklistComponent records={records} />, targetEl);
});

先に定義したコンポーネントは、57 行目にあるように ReactDOM.render() で表示できます。

62
ReactDOM.render(<ChecklistComponent records={records} />, targetEl);

今回は一覧表示イベントで表示したいため、一覧表示イベントハンドラーの中に定義しています。
コンポーネントには event オブジェクトに入っているレコードの一覧を渡しています。
今回のように、汎用的なコンポーネントをいくつか定義しておき、kintone のイベントハンドラーはそれを組み立てるだけの最低限で書くことも可能です。

おわりに

JavaScript カスタマイズは、今回のように React を活用したり、Vue などの他フレームワークやプレーンな JavaScript も利用できるなど、さまざまなカスタマイズ方法があります。

自分のプロジェクトにあったカスタマイズ方法は何かを見定めて技術選定をすることで、JavaScript カスタマイズの運用がスムーズになります。
特にある程度カスタマイズし続ける可能性が見込まれそうな場合、ぜひ今回のように React などの大きな規模にも耐えられるフレームワーク・ライブラリを利用してみるのもいいかと思います。

information

この Tips は、2021 年 9 月版 kintone で動作を確認しています。