Motomichi Works Blog

モトミチワークスブログです。その日学習したことについて書いている日記みたいなものです。

Chart.js 4.4.0でレスポンシブなチャートを作るときに参考になりそうなオプションのメモ

参照したページ

Chart.js 公式ドキュメント

jsDelivr

pluginの作り方

網羅的にまとめてあるページ

X軸のラベルを縦書きにするQiita記事

v2からv3になってX軸とY軸のオプションのオブジェクト構造が変更されていたことについてのQiita記事

はじめに

npmとか使わずに静的なhtmlにチャートを表示する必要に迫られて色々と調べました。

チャートのオプションは巨大なオブジェクトで、英語が読めないせいもあり、プロパティの意味がわからない場合が多くていつも辛い。

TypeScriptだったら楽かも。

基本的に Chart.js 4.4.0 の設定値について書いています。

バージョンによってプロパティ名やオブジェクト構造が違うので注意が必要です。

とりあえず棒グラフの例だけ書いていますが、後日他のチャートタイプについても書くかもしれません。

ついでに積み上げ棒グラフ(Stacked Bar Chart)の合計値を表示するプラグインを作成しました。

スクリーンショット

以下のような感じに表示されます。

PC

SP

公式ドキュメントに掲載されているオプションの例

最新バージョンの各チャートタイプのオプションの例は以下の公式ページで確認するとわかりやすかったです。

バージョンごとのオプションについては、ちょっと見難い感じもしますが以下のページなどで確認できます。

chart-js-examples.html の作成

まずhtmlファイルを作成します。
CSSも含めて以下の通り作成しました。

<html>
  <head>
    <style>
      .chart-wrapper {
        height: 360px;
        padding: 16px 8px;
        background-color: #f4f4f4;
        border: 1px solid #dddddd;
      }

      @media screen and (min-width: 992px) {
        .chart-wrapper {
            height: 680px;
        }
      }
    </style>
  </head>
  <body>
    <section class="chart-wrapper">
      <!-- タイトルの左にアイコン付けたいとかデザイン的な理由でここに( title )入れても良い -->
      <canvas id="bar-chart-example"></canvas>
    </section>

    <!-- cdn から chart.js を読み込む -->
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0/dist/chartjs-plugin-datalabels.min.js"></script>

    <!-- 自作のプラグインを読み込む -->
    <script src="./chartjs-plugin-total-labels.js"></script>

    <!-- bar-chart-example.js -->
    <script src="./bar-chart-example.js"></script>
  </body>
</html>

Stacked Bar Chart の合計値を表示するプラグインの作成

chartjs-plugin-total-labels.js を以下の記述内容で作成しました。

const ChartTotalLabels = {
  /**
   * options.plugins.{plugin-id} のルールで使用されるid
   * 詳しくは https://www.chartjs.org/docs/4.4.0/developers/plugins.html#plugin-id を参照してください。
   */
  id: 'totalLabels',

  /**
   * 合計値ラベルを描画
   */
  afterDatasetsDraw: function (chart, _args, options) {
    const font = options.font();
    const fontSize = Number(font.size.replace('px', ''));

    ctx = chart.ctx;
    // ラベルの書式設定
    ctx = this.setTextStyle(ctx, fontSize);
    // 各棒最後の項目のメタ情報を取得
    const meta = this.getLastMeta(chart);
    // 各合計値を取得
    const sums = this.calcSums(chart);
    // ラベル描画
    const labels = this.makeLabels(meta, sums, fontSize);
    labels.forEach(function (label) {
      ctx.fillText(label.value, label.x, label.y);
    });
  },

  /**
   * 各項目の合計を取得
   */
  calcSums: function (chart) {
    const valueList = [];
    chart.data.datasets.forEach(function (dataset, i) {
      var meta = chart.getDatasetMeta(i);

      // 非表示の項目は処理しない
      if (meta.hidden) {
        return;
      }
      dataset.data.forEach(function (value, j) {
        if (typeof valueList[j] === 'undefined') {
          valueList[j] = { sum: 0, groupByIndex: [] };
        }

        valueList[j].groupByIndex.push(value);
        valueList[j].sum = valueList[j].sum + value;
      });
    });

    const sums = valueList.map(function(valueItem) {
      const isWillNull = valueItem.sum === 0
        && valueItem.groupByIndex.every(function(item) { return item === null });

      return isWillNull ? null : valueItem.sum;
    })

    return sums;
  },

  /**
   * 各棒最後の項目のメタ情報を取得
   * (非表示のものは除く)
   */
  getLastMeta: function (chart) {
    let i = chart.data.datasets.length - 1;
    let meta = undefined;
    do {
      meta = chart.getDatasetMeta(i);
      i--;
    } while (meta.hidden && i >= 0);

    return meta;
  },

  /**
   * ラベル情報を取得
   */
  makeLabels: function (meta, sums, fontSize) {
    const labels = [];
    sums.forEach(function (sum, i) {
      const labelX = meta.data[i].x;
      const labelY = meta.data[i].y;
      labels.push({
        value: sum === null ? '' : sum.toString(),
        x: labelX,
        y: labelY - fontSize,
      });
    });

    return labels;
  },

  /**
   * 書式設定
   */
  setTextStyle: function (ctx, fontSize) {
    const fontStyle = "bold";
    const fontFamily = "'Helvetica Neue', Helvetica, Arial, sans-serif";
    ctx.font = Chart.helpers.fontString(fontSize, fontStyle, fontFamily);
    ctx.fillStyle = "#666";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";

    return ctx;
  }
};

棒グラフの例: ( bar-chart-example.js の作成 )

以下の通り記述しました。

(function () {
  // canvas要素を取得
  const ctx = document.getElementById('bar-chart-example');

  // ブレークポイント
  const BREAKPOINTS = {
    lg: 992,
  };

  // X軸のメモリラベル
  const DATA_LABELS = [
    ['10月', '2023年'], // 配列で渡すと改行される
    ['11月'],
    ['12月'],
    ['1月', '2024年'],
    ['2月'],
    ['3月'],
    ['4月'],
    ['5月'],
    ['6月'],
    ['7月'],
    ['8月'],
    ['9月'],
    ['10月'],
  ];

  // 関数
  function isWindowWidthLg() {
    return BREAKPOINTS.lg <= window.innerWidth;
  }

  // newする
  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: DATA_LABELS,
      datasets: [
        {
          backgroundColor: '#64b7ba',
          label: 'データセット01',
          data: [
            11111,
            4444,
            6666,
            9999,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
          ],
        },
        {
          backgroundColor: '#eab841',
          label: 'データセット02',
          data: [
            7614,
            8614,
            6614,
            9614,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
          ],
        },
      ],
    },
    options: {
      maintainAspectRatio: false, // 縦横比を自動調整するかどうか
      onResize: function(chartInstance, size) {
        // chartInstanceとsizeを引数で受け取れる
        // sizeはcanvasのstyle属性のwidthとheightに設定された値で、ctx.widthとctx.heightはcanvasのwidth属性とheight属性に設定された値です。詳細はブラウザの開発ツールで確認してください。
        console.log('chartInstance: ', chartInstance);
        console.log('size: ', size);
        console.log('ctx: ', ctx.height);
        console.log('ctx: ', ctx.width);
        console.log('ctx: ', ctx.style);

        // chartInstanceの値を上書きすることでレスポンシブ対応できる
        // あらかじめ取得しておいたctxのstyle属性なども、試してないけどたぶんここで上書きできると思う
        // fontの設定値はここではなく、各fontにコールバックを渡す方が簡単そう
        if (isWindowWidthLg()) {
          // Window幅がlg以上の場合の設定
          chartInstance.data.labels = DATA_LABELS;
          return;
        }

        // Window幅がlg未満の場合の設定
        const dataLabelsForMd = DATA_LABELS.map(function(item){ return item.reverse().join('') });
        chartInstance.data.labels = dataLabelsForMd;
      },
      plugins: {
        // チャート上に表示される値 ( 各datasetごとに別々の色を設定することも可能 )
        datalabels: {
          color: '#ffffff',
          font: function() {
            if (isWindowWidthLg()) {
              return {
                size: '12px',
              };
            }

            // Window幅がlg未満の場合の設定
            return {
              size: '5px',
            };
          },
        },
        totalLabels: {
          font: function() {
            if (isWindowWidthLg()) {
              return {
                size: '14px',
              };
            }

            // Window幅がlg未満の場合の設定
            return {
              size: '6px',
            };
          },
        },
        // 凡例
        legend: {
          display: true,
          labels: {
            font: function() {
              if (isWindowWidthLg()) {
                return {
                  weight: 'bold',
                  size: '16px',
                };
              }

              // Window幅がlg未満の場合の設定
              return {
                weight: 'bold',
                size: '10px',
              };
            },
            usePointStyle: true,
            pointStyle: 'circle', // (usePointStyleと一緒に設定しないと効果ないので注意)
          },
          position: 'bottom',
        },
        // タイトル (タイトルはhtmlとcssで普通に描画してもよさそう)
        title: {
          color: '#555555',
          display: true,
          font: function() {
            if (isWindowWidthLg()) {
              return {
                weight: 'bold',
                size: '24px',
              };
            }

            // Window幅がlg未満の場合の設定
            return {
              weight: 'bold',
              size: '18px',
            };
          },
          padding: {
            bottom: 20,
          },
          text: '棒グラフの例',
        },
      },
      scales: {
        y: {
          stacked: true, // 縦に積む場合はtrue
          suggestedMax: 25000, // 目盛りの最大値を大きめに設定したい場合など ( 本当に固定したい場合は max というプロパティもある )
          ticks: {
            stepSize: 5000, // 目盛り間隔
            color: '#555555', // 目盛りラベルの色
            font: function() {
              if (isWindowWidthLg()) {
                return {
                  size: '16px',
                  weight: 'bold',
                }
              }

              return {
                size: '10px',
                weight: 'bold',
              }
            },
          },
        },
        x: {
          stacked: true, // 縦に積む場合はtrue
          ticks: {
            autoSkip: false, // canvasの幅が狭い場合に目盛りを省略するかどうか ( 目盛りを省略せずに角度をつけて重ならないようにする選択肢もある )
            color: '#555555', // 目盛りラベルの色
            font: function() {
              if (isWindowWidthLg()) {
                return {
                  size: '14px',
                  weight: 'bold',
                }
              }

              return {
                size: '10px',
                weight: 'bold',
              }
            },
            // maxRotation: 90, // canvasの幅が狭いときなどに角度をつけて重ならないようにする
            // minRotation: 90, // canvasの幅が狭いときなどに角度をつけて重ならないようにする
          },
        },
      },
    },
    // 各種プラグインを使用する場合は配列に入れる
    plugins: [
      ChartDataLabels,
      ChartTotalLabels,
    ],
  });
})();

General ( 深いところにある共通プロパティの設定 )

fontの設定

fontにはコールバック関数を渡すこともできるので、レスポンシブ対応に便利そうです。

paddingの設定

options

レスポンシブ関連のプロパティ

options.plugins.legendに設定可能なプロパティ ( 凡例の設定 )

options.plugins.scalesに設定可能なプロパティ ( X軸とY軸の設定など )

MacでPATHを通す( npmでインストールしたyarnにpathを通す )

参照したページ

はじめに

まず前提として、

  • brew を使って nodenv をインストール済みです。
  • nodenv を使って node をインストール済みです。

例として npm で yarn をインストールして path を通します。

他のソフトウェアにpathを通す場合も応用が利くと思います。

node のバージョン確認

node のバージョンを確認します。

node -v

node のパスを確認

以下のコマンドを実行します。

which node

上記のコマンドを実行して、以下のように表示されました。

/Users/ユーザー名/.nodenv/shims/node

yarn をインストールする。

この記事では yarn をインストールして path を通します。

npm install -g yarn

yarn コマンドが使えるか試してみる

以下のコマンドを実行してみます。

yarn -v

これでバージョンが表示されれば、特にやることはありませんが、 zsh: command not found: yarn と表示されるようであれば、pathを通す必要があります。

.zshrc にパス設定を追記

.zshrc を開きます。

vim ~/.zshrc

以下の行を追記します。

PATH=/Users/motomichishigeru/.nodenv/shims/node:$PATH
export PATH

.zshrcの変更を反映する

以下のコマンドで設定を反映します。

source ~/.zshrc

環境変数 $PATH を確認します。

printenv PATH

または

echo $PATH

で確認できます。

コマンドが使えるかもう一度試してみる

以下のコマンドを実行してみます。

yarn -v

以下のようにバージョンが表示されるようになりました。

1.22.19

Next.jsのアプリケーションにnice-modal-reactを導入してみる

参照したページ

今日の環境

  • ebay/nice-modal-react: 1.2.10
  • next: 13.4.4

はじめに

Next.jsのアプリケーションにnice-modal-reactを使って、実用的なモーダルを実装していきます。

Next.jsのApp Routerは使わないアプリケーションにnice-modal-reactを導入する例です。

nice-modal-reactをインストールする

以下のコマンドを実行しました。

yarn add @ebay/nice-modal-react

src/components/Modal.tsxの作成

Modalコンポーネントを作成しました。
汎用的に使用するモーダルの枠です。CSSはstyled-jsxで書いています。

以下の内容で作成しました。通常のコンポーネントと違って、NiceModal.create()の戻り値をexportしています。

import React from "react";
import css from "styled-jsx/css";
import * as NiceModal from "@ebay/nice-modal-react";

type Props = {
  children: React.ReactNode;
  isVisible: boolean;
  onOverlayClick: () => void;
};

export const Modal = NiceModal.create(function Modal({
  children,
  isVisible,
  onOverlayClick,
}: Props) {
  const [modifierClassName, setModifierClassName] = React.useState("");

  React.useEffect(() => {
    const modifierClassName = isVisible ? "is-visible" : "";
    setModifierClassName(modifierClassName);
  }, [isVisible]);

  return (
    <div className={`modal ${modifierClassName}`}>
      <div className="modal-overlay" onClick={onOverlayClick}>
        &nbsp;
      </div>
      {children}
      <style jsx>{styles}</style>
    </div>
  );
});

const styles = css`
  .modal {
    position: fixed;
    top: 0;
    left: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100vw;
    height: 100vh;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.5s;
  }

  .modal.is-visible {
    pointer-events: initial;
    opacity: 1;
  }

  .modal-overlay {
    position: fixed;
    width: 100vw;
    height: 100vh;
    background-color: #000;
    opacity: 0.5;
  }
`;

src/components/ExampleModalContents.tsxの作成

ExampleModalContentsコンポーネントを以下の内容で作成しました。

import React from "react";
import css from "styled-jsx/css";

type Props = {
  children: React.ReactNode;
};

export const ExampleModalContents = React.memo(function ExampleModalContents({
  children,
}: Props) {
  return (
    <div className="example-modal-contents">
      {children}
      <style jsx>{styles}</style>
    </div>
  );
});

const styles = css`
  .example-modal-contents {
    position: fixed;
    width: 80vw;
    height: 80vh;
    background-color: #fff;
  }
`;

src/pages/_app.tsx の編集

src/pages/_app.tsxには、個々の環境によって、色々書いてあるとは思いますが、以下のことをします。

  • NiceModalをimport
  • <NiceModal.Provider>でwrapする

例としては以下のようになります。

import type { AppProps } from "next/app";
import * as NiceModal from "@ebay/nice-modal-react";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <NiceModal.Provider>
      <Component {...pageProps} />
    </NiceModal.Provider>
  );
}

src/pages/index.tsx の編集

モーダルを使用する場所はどこでも良いのですが、例として src/pages/index.tsx を以下のように編集しました。
useModal()の引数とModalコンポーネントのid属性には同じ文字列を渡します。

import * as NiceModal from "@ebay/nice-modal-react";
import { Modal } from "@/components/Modal";
import { ExampleModalContents } from "@/components/ExampleModalContents";

export default function Home() {
  const modalId = "example-modal";
  const modal = NiceModal.useModal(modalId);

  const showModal = () => {
    void modal.show();
  };

  const onCancelButtonClick = () => {
    void modal.hide();
  };

  const onApplyButtonClick = () => {
    void modal.hide();
  };

  return (
    <div>
      <button onClick={showModal}>モーダルを開く</button>

      <Modal
        id={modalId}
        isVisible={modal.visible}
        onOverlayClick={onCancelButtonClick}
      >
        <ExampleModalContents>
          <button onClick={onCancelButtonClick}>キャンセル</button>
          <button onClick={onApplyButtonClick}>適用</button>
        </ExampleModalContents>
      </Modal>
    </div>
  );
}

modal.show()のPromiseを使いたい場合

例えば以下のようにすると、modal.show() が返している Promise を使って後続の処理を実行することもできます。

  const showModal = () => {
    modal
      .show()
      .then((result) => {
        console.log('result: ', result);
      })
      .catch((error) => {
        console.log('error: ', error);
      });
  };

  const onOverlayClick = () => {
    modal.resolve({ message: 'resolved foo' });
    void modal.hide();
  };

iPhone 12 miniの画面のピクセルサイズ

はじめに

自分の備忘のためですが、iPhoneの画面幅を検索するとページによって書いてある数値が違ったので手元にある実機で調べてみました。

幅と高さ

機種 高さ(アドレスバー表示時)
iPhone 12 mini 375px 629px
iPhone 14 390px 664px

Next.jsでuseEffectのクリーンアップ関数はクリーンアップのために使うという当たり前の話

参照したページ

はじめに

この記事は個人的な学習記録です。

useEffectのクリーンアップ関数の駄目な使い方について書いています。

クリーンアップ関数については公式の 副作用フックの利用法 – React あたりを読むと良さそうです。

コンポーネントの実行順序については Hooks時代のReactライフサイクル完全理解への道 を読むとわかりやすいのではないかと思います。

この記事を書いたきっかけは、useEffectのトリガーになった変数の「変更前の値」を取得したい。と思って調べていたときに、Reactの公式ドキュメントの「フックに関するよくある質問 – React」のページからリンクされている「前回の props や state はどうすれば取得できますか?」のページに掲載されている、エフェクトのクリーンアップ関数を使用した以下のようなサンプルコードの意味がわからなかったので、関数コンポーネントの処理の実行順序や、エフェクトのクリーンアップ関数の用途について学習したことがきっかけです。

useEffect(() => {
  ChatAPI.subscribeToSocket(props.userId);
  return () => ChatAPI.unsubscribeFromSocket(props.userId);
}, [props.userId]);

結局のところ「前回の props や state はどうすれば取得できますか?」というタイトルに惑わされましたが、自分が欲しかったのは上記のようなuseEffectのクリーンアップ関数を使う手法ではなく、単純に以下のように任意のタイミングで値を保持することでした。

const [ prevValue, setPrevValue ] = useState('');

あとは、同ページに以下のように記載がある、usePreviousを作成する方法もよく検索でヒットします。

これまで usePrevious というカスタムフックを使うことを提案していましたが、ほとんどのユースケースは上記の 2 つのパターンのいずれかに当てはまることがわかりました。

このように記載されているということは、usePrevious というカスタムフックを使う手法は現在はあまり推奨されていないように感じますがよくヒットしますね。

クリーンアップ関数については、当たり前のことなのですが、クリーンアップ関数なんだからクリーンアップの為だけに使う。
コンポーネントをunmountしただけでは残ってしまうような、useEffect()のコールバックで追加したイベントリスナーを破棄するとか、前回の値を使って元に戻すような処理をするということであって、「クリーンアップ関数を使って前回の値をどこかに保存するということではない。」ということですね。

この記事では「クリーンアップ関数を使って前回の値をどこかに保存してしまう」という「駄目な例」について書いています。

駄目な例

駄目なChild1.tsxの作成とその記述内容

Child1 コンポーネントを以下の記述内容で作成します。
あくまでこれは駄目な実装例です。

import React from 'react';

type Props = {
  currentStr: string;
};

export function Child1({ currentStr }: Props) {
  console.log('処理 子A');
  const [child1PrevStr, setChild1PrevStr] = React.useState('defaultChild1PrevStr');

  React.useEffect(() => {
    console.log('処理 子B');
  }, []);

  React.useEffect(() => {
    console.log('処理 子C-01');
    console.log('currentStr: ', currentStr);

    return () => {
      console.log('処理 子C-02');
      setChild1PrevStr(currentStr);
    };
  }, [currentStr]);

  React.useEffect(() => {
    console.log('処理 子D');
    console.log('child1PrevStr: ', child1PrevStr);
    console.log('currentStr: ', currentStr);
  }, [child1PrevStr, currentStr]);

  function getChild1Text() {
    console.log('処理 子E');
    return 'text';
  }

  return (
    <div>
      <div>Child1: child1PrevStr: {child1PrevStr}</div>
      <div>Child1: currentStr: {currentStr}</div>
      <div>{getChild1Text()}</div>
    </div>
  );
}

Parent.tsxの作成とその記述内容

Parent コンポーネントを以下の記述内容で作成します。

import React from 'react';
import { Child1 } from './Child1';

export function Parent() {
  console.log('処理 親A');
  const [str, setStr] = React.useState('defaultParentStr');

  React.useEffect(() => {
    console.log('処理 親B');
  }, []);

  function getParentText() {
    console.log('処理 親C');
    return 'parentText';
  }

  return (
    <div>
      <Child1 currentStr={str} />
      <div>{getParentText()}</div>
    </div>
  );
}

Parent.tsxをpages/index.tsxに配置する

以下の通り pages/index.tsx を記述します。

import { Parent } from '@/components/index/template/Parent';

export default function Home() {
  return <Parent />;
}

yarn devで実行してみる

yarn devで実行して、ブラウザをリロードしてみます。

コンソールログの出力結果

コンソールには以下のように出力されました。

  • Parent.tsx:5 処理 親A
  • Parent.tsx:13 処理 親C
  • installHook.js:342 処理 親A
  • installHook.js:342 処理 親C
  • Child1.tsx:8 処理 子A
  • Child1.tsx:32 処理 子E
  • installHook.js:342 処理 子A
  • installHook.js:342 処理 子E
  • Child1.tsx:12 処理 子B
  • Child1.tsx:16 処理 子C-01
  • Child1.tsx:17 currentStr: defaultParentStr
  • Child1.tsx:26 処理 子D
  • Child1.tsx:27 child1PrevStr: defaultChild1PrevStr
  • Child1.tsx:28 currentStr: defaultParentStr
  • Parent.tsx:9 処理 親B
  • Child1.tsx:20 処理 子C-02
  • Child1.tsx:12 処理 子B
  • Child1.tsx:16 処理 子C-01
  • Child1.tsx:17 currentStr: defaultParentStr
  • Child1.tsx:26 処理 子D
  • Child1.tsx:27 child1PrevStr: defaultChild1PrevStr
  • Child1.tsx:28 currentStr: defaultParentStr
  • Parent.tsx:9 処理 親B
  • Child1.tsx:8 処理 子A
  • Child1.tsx:32 処理 子E
  • react_devtools_backend_compact.js:2421 処理 子A
  • react_devtools_backend_compact.js:2421 処理 子E
  • Child1.tsx:26 処理 子D
  • Child1.tsx:27 child1PrevStr: defaultParentStr
  • Child1.tsx:28 currentStr: defaultParentStr

コンソールログの出力結果を読んでみる

installHook.jsとreact_devtools_backend_compact.jsの実行を省いて処理を読んでみます。

  1. 処理 親A
  2. 処理 親C
    • 親が実行された。
  3. 処理 子A
  4. 処理 子E
    • 子が実行された。
      • このときuseEffect()のコールバックは後まわし
  5. 処理 子B
    • 子のuseEffect()のコールバックが実行された。
      • 第二引数に [] を渡したuseEffect()のコールバック
  6. 処理 子C-01
    • 子のuseEffect()のコールバックが実行された。
      • 親のuseEffect()のコールバックより先に実行された。
      • 第二引数に [currentStr] を渡したuseEffect()のコールバック
      • このとき、親から受け取った props.currentStr は 'defaultParentStr'
  7. 処理 子D
    • 子のuseEffect()のコールバックが実行された。
      • 親のuseEffect()のコールバックより先に実行された。
      • 第二引数に [child1PrevStr, currentStr] を渡したuseEffect()のコールバック
      • このとき、変数 child1PrevStr は 'defaultChild1PrevStr'
      • このとき、親から受け取った props.currentStr は 'defaultParentStr'
  8. 処理 親B
    • 親のuseEffect()のコールバックが実行された。
      • 第二引数に [] を渡したuseEffect()のコールバック
  9. 処理 子C-02
    • 子のuseEffect()のコールバックのクリーンアップ関数が実行された。
      • このとき変数 child1PrevStr には「7」の 'defaultParentStr' で上書きされる
  10. 処理 子B(上記の「5」と同じ処理)
  11. 処理 子C-01(上記の「6」と同じ処理)
  12. 処理 子D(上記の「7」と同じ処理)
  13. 処理 親B(上記の「8」と同じ処理)
  14. 処理 子A
  15. 処理 子E
    • 上記の「9」によって子のstate child1PrevStrが更新されたので子が再実行された。
  16. 処理 子D
    • 子のuseEffect()のコールバックが実行された。
      • 第二引数に [child1PrevStr, currentStr] を渡したuseEffect()のコールバック
      • 上記の「9」によって依存配列の child1PrevStr が更新されたのでコールバックが実行された。
      • このとき、変数 child1PrevStr は 'defaultParentStr'
      • このとき、親から受け取った props.currentStr は当然変化なしで 'defaultParentStr'

ここまで思ったこと

  • なぜ「5〜8」と同じ処理が「10〜13」で再実行されるのか
    • サーバーサイドとクライアントサイドで実行しているから
      • それなら yarn build + yarn start で起動したら1回になりそう
  • なぜ「9」は実行されてしまうのか
    • クリーンアップ関数は、子コンポーネントをunmountするときまたは、「処理 子C-01」のエフェクト実行前にも実行される
      • どっちの条件によるものかはたぶん後者
  • 「9」が実行されることによって意図していない「14〜16」が実行されている
    • 「9」のクリーンアップ処理で setChild1PrevStr(currentStr); していたのをやめると「14〜16」が実行されなくなるのでこれは明らか。

yarn build + yarn start で実行してみる

コンソールログの出力結果を読んでみる

  • 処理 親A
  • 処理 親C
  • 処理 子A
  • 処理 子E
  • 処理 子B
  • 処理 子C-01
    • currentStr: defaultParentStr
  • 処理 子D
    • child1PrevStr: defaultChild1PrevStr
    • currentStr: defaultParentStr
  • 処理 親B

ここまでで思ったこと

  • 当初実装したかった挙動になった。
  • よく理解せずにuseEffectのクリーンアップ関数を使うとdevとprodで違う挙動になる。
    • クリーンアップ関数なのだから、unmountしただけでは残ってしまうものをクリーンアップするためにだけ使う。
    • この記事の「駄目なChild1.tsx」のコードは、副作用にクリーンアップ関数を使ってさらなる副作用をかけている。

installHook.jsとは

javascript - What is chrome-extension:// installHook.js that is being downloaded for my pages - Stack Overflow によると、React Dev Tools Chrome 拡張機能 に含まれているファイルらしいです。

React.memoで子コンポーネントの再描画を抑制する

関数コンポーネントの再描画とは

関数が実行されて、HTML文書構造をreturnすることで、対象のDOMが再描画されます。

関数コンポーネントが再描画(再実行)されるタイミング

以下のようなときに再描画(再実行)されます。

  1. stateが変化した時
  2. propsが変化した時
  3. コンポーネントが再描画された時

React.memoについて

React.memoを使うと関数コンポーネントをメモ化することができ、上記した3の場合について、無駄な再描画を減らすことができます。

サンプルコード

例えば以下のようなParent.tsxHoge.tsx、Foo.tsxを作成します。

Hoge.tsxとFoo.tsxはメモ化したうえで、子コンポーネントとして使用します。

ボタンをクリックして親コンポーネントで定義したstateを変化させ、これら3つのコンポーネントの再描画の挙動について確認していきます。

Parent.tsx

import React from 'react';
import { Hoge } from '@/components/common/elements/Hoge';
import { Foo } from '@/components/common/elements/Foo';

export function Parent() {
  const [hoge, setHoge] = React.useState(0);
  const [foo, setFoo] = React.useState(0);

  function incrementHoge() {
    setHoge(hoge + 1);
  }

  function incrementFoo() {
    setFoo(foo + 1);
  }

  console.log('Parentを再実行します。');

  return (
    <>
      <button onClick={incrementHoge}>hoge + 1</button>
      <button onClick={incrementFoo}>foo + 1</button>
      <Hoge hoge={hoge} />
      <Foo foo={foo} />
    </>
  );
}

Hoge.tsx

import React from 'react';

type Props = {
  hoge: number;
};

export const Hoge = React.memo(function Tmp({ hoge }: Props) {
  console.log('Hogeを再実行します。');

  return <div>{hoge}</div>;
});

Foo.tsx

import React from 'react';

type Props = {
  foo: number;
};

export const Foo = React.memo(function Tmp({ foo }: Props) {
  console.log('Fooを再実行します。');

  return <div>{foo}</div>;
});

コードの解説

コンポーネントをメモ化していない場合、 hoge + 1 ボタンをクリックすると、実行結果に変化がないFoo.tsxまで再実行されてしまいます。

これは上記した「関数コンポーネントが再描画(再実行)されるタイミング」の「3. 親コンポーネントが再描画された時」に該当するためです。

このサンプルコードのように子コンポーネントをメモ化することで、 hoge + 1 ボタンをクリックしたときに、実行結果に変化がないFoo.tsxは再実行されないことがわかります。

同様に foo + 1 ボタンをクリックした場合には、実行結果に変化がないHoge.tsxは再実行されないことがわかります。

dockerを使ってOpenAPIのファイルからコードをgenerateする | openapitools/openapi-generator-cli

参照したページ

ディレクトリ構造の例

この記事では、カレントディレクトリに local ディレクトリがある以下の構造を想定しています。

./
└── local
    └── sample.yml

sample.yml の記述内容は、OpenAPIのフォーマットに則って記述しているものとします。

generateする

公式ページの CLI Installation | OpenAPI Generator を参考に、以下のコマンドを実行します。

docker run --rm \
  -v ${PWD}/local:/local openapitools/openapi-generator-cli generate \
  -i /local/sample.yml \
  -g typescript-fetch \
  -o /local/out/typescript-fetch