Techtouch Developers Blog

テックタッチ株式会社の開発チームによるテックブログです

Chart.jsをReactにProviderを使用して組み込んだシンプルなサンプル

adventCalendar2021-day18

この記事はテックタッチアドベントカレンダー18日目の記事です。

はじめまして、テックタッチアドベントカレンダー12月18日担当のyassanですよろしくお願いいたします。 普段は、テックタッチの管理画面を中心に、フロントエンドエンジニアとして業務に携わっています。

本稿では、業務で使用したChart.jsをReactにProviderを使用して組み込んだ際の備忘録として、簡潔にサンプルコードをまとめていきたいと思います。

Chart.jsとReactの間のデータの流れを中心に取り上げていこうと思います。 Chart.js自身のカスタマイズなどは取り扱いません。

主に扱うライブラリに関して

取り扱うライブラリに関して、念の為の補足をいたします。

React Meta(旧Facebook)が開発したユーザーインターフェイス構築のためのJavaScriptライブラリです。

Chart.js JavaScript でグラフ(チャート)を描画するためのライブラリです。 あまり複雑な手順を取らずに、綺麗なグラフを描画することができます。

react-chartjs-2 Chart.jsを組み込んだReactのコンポーネントライブラリです。

サンプルに関して

f:id:techtouch:20211216101646g:plain
Chart.js・Reactサンプル

Next.jsとTypeScriptで作成しています。 またreact-query・constateの2つのライブラリも追加しています。 今回作ったサンプルは3つのチェックボックスと連動して、異なるAPIから取得したデータを折れ線グラフで表示させます。 Chart.jsとReactのプロバイダー機能を用いたシンプルな実装です。

ディレクトリ構成

.
├── pages
│   ├── index.js // コンポーネントをまとめて表示させます
│   └── api // サンプルのAPIを作成しています
│    ├── sampleA.ts
│    ├── sampleB.ts
│    └── sampleC.ts
│
├── components
│   ├── LineGraph.tsx // グラフ
│   └── DataTypeCheckbox.tsx // グラフ選択チェックボックス
│
├── queries //Data Fetching
│    ├── sampleA.ts
│    ├── sampleB.ts
│    └── sampleC.ts
│
├── providers // Hooks
│    ├── datasets.ts
│    ├── dataTypes.ts
│    ├── sampleA.ts
│    ├── sampleB.ts
│    └── sampleC.ts
│
├── types
│    └── index.ts
│
...

コード

以下、サンプルコードとなります。 サンプルですので冗長だったり、この規模では分割しすぎている箇所などもありますが、参考程度にご確認していただければ幸いです。

page/index.ts

表示されるページ。 各プロバイダー・コンポーネントを組み込んでいます。

import { QueryClient, QueryClientProvider } from 'react-query'
import { SampleAProvider } from '../providers/sampleA'
import { SampleBProvider } from '../providers/sampleB'
import { SampleCProvider } from '../providers/sampleC'
import { DataTypesProvider } from '../providers/dataTypes'
import { DatasetsProvider } from '../providers/datasets'
import { LineGraph } from '../components/LineGraph'
import { DataTypeCheckbox } from '../components/DataTypeCheckbox'

export default function Home() {
  const queryClient = new QueryClient()
  return (
    <QueryClientProvider client={queryClient}>
      <SampleAProvider>
        <SampleBProvider>
          <SampleCProvider>
            <DataTypesProvider>
              <DatasetsProvider>
                <LineGraph />
                <div>
                  <DataTypeCheckbox title="sampleA" dataType="sampleA" />
                  <DataTypeCheckbox title="sampleB" dataType="sampleB" />
                  <DataTypeCheckbox title="sampleC" dataType="sampleC" />
                </div>
              </DatasetsProvider>
            </DataTypesProvider>
          </SampleCProvider>
        </SampleBProvider>
      </SampleAProvider>
    </QueryClientProvider>
  )
}
pages/api/sampleA.ts

NextJSを使用したサンプルのAPIデータ。 sampleB,sampleCはsampleAのData違い。

import type { NextApiRequest, NextApiResponse } from 'next'
type Response = {
  data: number[]
}
export default function sampleA(
  req: NextApiRequest,
  res: NextApiResponse<Response>,
): void {
  const data = [65, 59, 80, 81, 56, 55, 40, 10, 60, 87, 12, 109]
  res.status(200).json({ data })
}
components/DataTypeCheckbox.tsx

表示するデータを選択するチェックボックス。

import { useMemo, useCallback } from 'react'
import { DataType } from '../types'
import { useOnCheckDataTypes, useDataTypes } from '../providers/dataTypes'
export const DataTypeCheckbox = ({
  title,
  dataType,
}: {
  title: string
  dataType: DataType
}) => {
  const dataTypes = useDataTypes()
  const onCheckDataTypes = useOnCheckDataTypes()
  const checked = useMemo(() => {
    return dataTypes.some((checked) => checked === dataType)
  }, [dataTypes, dataType])
  const onChange = useCallback(() => {
    onCheckDataTypes(dataType)
  }, [onCheckDataTypes, dataType])
  return (
    <label>
      {title}:
      <input defaultChecked={checked} onChange={onChange} type="checkbox" />
    </label>
  )
}
components/LineGraph.tsx

折れ線グラフコンポーネント。

import { Line } from 'react-chartjs-2'
import { useDatasets } from '../providers/datasets'

export const LineGraph = () => {
  const datasets = useDatasets()
  const data = {
    labels: [
      '1月',
      '2月',
      '3月',
      '4月',
      '5月',
      '6月',
      '7月',
      '8月',
      '9月',
      '10月',
      '11月',
      '12月',
    ],
    datasets,
  }

  return <Line data={data} height={240} width={1024} />
}
queries/sampleA.ts

sampleAのAPIデータをfetch。 sampleB,sampleCはsampleAのData違い。

import { useQuery } from 'react-query'
import { UseQueryResult } from 'react-query'
export const useSampleAQuery = (): UseQueryResult<{ data: number[] }> => {
  return useQuery('sampleA', async () => {
    const res = await fetch('/api/sampleA')
    return res.json()
  })
}
providers/datasets.ts

グラフ用データセット。

import constate from 'constate'
import { DataSetsType } from '../types'
import { useSampleA } from './sampleA'
import { useSampleB } from './sampleB'
import { useSampleC } from './sampleC'
import { useDataTypes } from './dataTypes'

export const [DatasetsProvider, useDatasets] = constate(
  () => {
    const sampleA = useSampleA()
    const sampleB = useSampleB()
    const sampleC = useSampleC()
    const dataTypes = useDataTypes()

    const sampleData: DataSetsType = { sampleA, sampleB, sampleC }

    const colors = ['rgb(75, 192, 192)', 'rgb(234, 123, 3)', 'rgb(65, 31, 234)']

    const selectedData = dataTypes.map((dataType, index) => ({
      data: sampleData[dataType],
      label: dataType as string,
      borderColor: colors[index],
    }))

    return {
      selectedData,
    }
  },
  (props) => props.selectedData,
)
providers/dataTypes.ts

表示するデータのState処理。

import constate from 'constate'
import { useState, useCallback } from 'react'
import { DataType } from '../types'
export const [DataTypesProvider, useDataTypes, useOnCheckDataTypes] = constate(
  () => {
    const [dataTypes, setDataTypes] = useState<DataType[]>(['sampleA'])

    const onCheck = useCallback(
      (dataType: DataType) => {
        if (dataTypes.some((checked) => checked === dataType)) {
          setDataTypes(dataTypes.filter((checked) => checked !== dataType))
          return
        }
        setDataTypes([...dataTypes, dataType])
      },
      [dataTypes],
    )
    return {
      dataTypes,
      onCheck,
    }
  },
  (props) => props.dataTypes,
  (props) => props.onCheck,
)
providers/sampleA.ts

sampleB,sampleCは以下略。

import constate from 'constate'
import { useSampleAQuery } from '../queries/sampleA'

export const [SampleAProvider, useSampleA, useSampleAIsLoading] = constate(
  () => {
    const { data, isLoading } = useSampleAQuery()
    return {
      data: data?.data || [],
      isLoading,
    }
  },
  (props) => props.data,
  (props) => props.isLoading,
)
types/index.ts
export type DataType = 'sampleA' | 'sampleB' | 'sampleC'

export type DataSetsType = {
  [key in DataType]: number[]
}

まとめ

queriesで各APIのfetchを行い、providersでコンポーネント間で共有するデータをqueriesのデータを含めて管理しています。 実際にはlabelsもProviderを経由していて、グラフの表示の日付の範囲を指定するコンポーネントと連動していたり、日別・月別でデータが切り替わったりと、グラフに表示されるデータは複雑に切り替わります。 Providerのデータもグラフだけではなく、リスト一覧表示や合計値の出力、CSVの出力など複数のコンポーネントで利用されています。 Providerを経由することでデータの状態管理の一貫性とコードの可読性が上がり、Propsでデータを引き回すより、後で追加される実装やデータに対応しやすくなりました。

おわりに

私自身、VueやAngularなど他のSPAフレームワークの経験もあり、書き方の違いはあれど、コンポーネント間の状態管理はSPAフレームワークでは必須だと思っています。 関数コンポーネント以降のReactについてはテックタッチにジョインした後経験しました。 個人の慣れや習熟度も関係してくるとは思いますが、Reduxを使用していた頃より、React QueryやProviderを用いる方が個人的にはシンプルで分かりやすいと思いました。 Reactが登場してからまだ8年ほどですが、SPAフレームワークの選択肢も増え、React自身も大きく変わっています。 また数年すれば、違うフレームワークが流行りだしているかもしれません。 時代に取り残されないよう情報をキャッチアップし続けていけたらと思います。