目指せ!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で動作を確認しています。