未経験からエンジニア 奮闘記

未経験からエンジニアに自由に生きる途中

MENU

React Reduceの使い方をマスターしよう!

背景

JavaScriptreduceというメソッドは、存在は知っていても実際にどのような場面で使うのかがわかりにくいことがあります。私も以前はその一人でしたが、最近購入履歴のデータを加工してチャートに反映する機会があり、その時にreduceの便利さを実感しました。具体的には、Rechartsを使ってグラフを描くために、購入履歴データを集計する必要がありました。

jsのreduceの挙動

まず、reduceの基本的な挙動について説明いたします。reduceは配列を一つの値にまとめるための関数ですが、初期値(initialValue)が数字の場合とオブジェクトの場合で少し使い方が変わります。

初期値が数字の場合

例えば、配列内の数字の合計を求める場合は以下のようにします:

const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, current) => acc + current, 0);
console.log(sum); // 10

ここで、accは累積値で、currentが現在の要素です。initialValueとして0を渡しているので、最初のaccは0からスタートします。

初期値がオブジェクトの場合

次に、初期値がオブジェクトの場合を見てみましょう。例えば、購入履歴をカテゴリごとに集計したい場合は以下のようになります:

const purchaseHistories = [
  { id: 1, amount: 100, category: { name: '外食' } },
  { id: 2, amount: 50, category: { name: '交際費' } },
  { id: 3, amount: 100, category: { name: 'Netflix' } },
  { id: 4, amount: 150, category: { name: '外食' } },
];

const categoryTotals = purchaseHistories.reduce((acc, history) => {
  const category = history.category.name;
  if (!acc[category]) {
    acc[category] = 0;
  }
  acc[category] += history.amount;
  return acc;
}, {});

console.log(categoryTotals);
// { '外食': 250, '交際費': 50, 'Netflix': 100 }

ここで、初期値として空のオブジェクト{}を渡しているため、各カテゴリの合計金額を計算することができます。

実際に利用した場面

次に、実際にどのように使ったかを見てみましょう。

上記のreduceを使って集計したデータを元に、Rechartsで画像の様なパイチャートを描きます。 以下のコードをご覧ください:

'use client'
import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'

const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042']
const RADIAN = Math.PI / 180

interface CustomizedLabelProps {
  cx: number
  cy: number
  midAngle: number
  innerRadius: number
  outerRadius: number
  percent: number
  index: number
  name: string
  value: number
}

const renderCustomizedLabel = ({
  cx,
  cy,
  midAngle,
  innerRadius,
  outerRadius,
  percent,
  index,
  name,
  value,
}: CustomizedLabelProps) => {
  const radius = innerRadius + (outerRadius - innerRadius) * 0.5
  const x = cx + radius * Math.cos(-midAngle * RADIAN)
  const y = cy + radius * Math.sin(-midAngle * RADIAN)

  return (
    <text
      x={x}
      y={y}
      fill="white"
      textAnchor={x > cx ? 'start' : 'end'}
      dominantBaseline="central"
      fontSize="13px"
    >
      {`${name} ${(percent * 100).toFixed(0)}%`}
    </text>
  )
}

const purchaseHistories = [
  {id: 1, amount: 100 ,category: { name: '外食'}},
  {id: 2, amount: 50 ,category: { name: '交際費'}},
  {id: 3, amount: 100 ,category: { name: 'Netflix'}},
  {id: 4, amount: 150 ,category: { name: '外食'}},
]

const RechartPipeChart = () => {
  const groupByCategoryPurchaseHistories = () => {
    const categoryTotals = purchaseHistories.reduce(
      (acc: Record<string, number>, history) => {
        const category = history.category.name
        if (!acc[category]) {
          acc[category] = 0
        }
        acc[category] += history.amount
        return acc
      },
      {},
    )

    return Object.keys(categoryTotals).map((category) => ({
      name: category,
      value: categoryTotals[category],
    }))
  }

  const groupedData = groupByCategoryPurchaseHistories()

  return (
    <>
      <ResponsiveContainer width="100%" height={300}>
        <PieChart width={500} height={500}>
          <Pie
            data={groupedData}
            cx="50%"
            cy="50%"
            labelLine={false}
            label={({
              cx,
              cy,
              midAngle,
              innerRadius,
              outerRadius,
              percent,
              index,
            }) =>
              renderCustomizedLabel({
                cx,
                cy,
                midAngle,
                innerRadius,
                outerRadius,
                percent,
                index,
                name: groupedData[index].name,
                value: groupedData[index].value,
              })
            }
            outerRadius={150}
            fill="#8884d8"
            dataKey="value"
          >
            {groupedData.map((entry, index) => (
              <Cell
                key={`cell-${index}`}
                fill={COLORS[index % COLORS.length]}
              />
            ))}
          </Pie>
        </PieChart>
      </ResponsiveContainer>
    </>
  )
}

export default RechartPipeChart

ここでは、purchaseHistoriesreduceを使ってカテゴリごとに集計し、その結果をmapでRechartsが扱いやすい形に変換しています。これにより、購入履歴のデータを元にしたパイチャートを表示することができます。

まとめ

reduceは一見難しそうに見えますが、使い方を覚えるとデータを集計したり変換したりするのに非常に便利です。特に初期値をうまく設定することで、様々なデータの操作が可能になります。今回の例では、購入履歴のデータをカテゴリごとに集計してチャートに反映する方法をご紹介しましたが、他にも多くの場面で応用できますので、ぜひ試してみてください。