Next.js と TypeScript で、React の仮想DOMを理解する

TwitterFacebookHatena

TL;DR

このページでは、React の仮想 DOM の実装方法について解説しますね。一言で言えば、仮想 DOM とは React がパフォーマンスを高めるために使用する効率的な更新手法です。

開発環境 バージョン
Next.js 13.4.4
TypeScript 5.0.4
Emotion 11.11.0
React 18.2.0

仮想 DOM とは?

まずはじめに、React が提供している仮想 DOM という考え方について理解しましょう。

みなさんがお絵かきをするときに、ちょっと難しい絵を描きたいと思ったことはありますか?例えば、大きな城や美しい風景などです。そのような複雑な絵を一発で完璧に描くのは難しいですよね。

そんなとき、どうしたらいいでしょう?まず、下書きをすると思います。下書きでは、全体の形をざっくりと描いたり、どこに何を描くかを決めたりします。そして、その下書きを見ながら、本番の絵を少しずつ描いていくと、難しい絵でも上手に描くことができます。

仮想 DOM(Virtual DOM)は、その下書きのようなものです。コンピュータがウェブサイトを描くときに、まず仮想 DOM という下書きを作ります。そして、その下書きを見ながら本当のウェブサイト(本番の絵)を描きます。これにより、効率よく美しいウェブサイトを描くことができます。

また、ウェブサイトで何か更新があったときには、全部を描き直すのではなく、変更があった部分だけ新しく描きます。これも下書きの力を利用しています。下書きと本番の絵を比較し、違いがある部分だけを新しく描くのです。

このように、仮想 DOM はコンピュータがウェブサイトを効率よく描くための下書きのようなものです。それが「仮想 DOM」という名前の由来でもあります。

それでは、ネイティブな React を使って仮想 DOM の仕組みを解説しましょう。そのために、とても簡単な React のアプリケーションを作成します。

JavaScript フレームワークである React は、ユーザーインターフェースの作成を助けます。React が特に優れている部分の一つは、仮想 DOM という機能を通じて、ウェブページのレンダリングを最適化することです。

実際の DOM(Document Object Model)は、ウェブページの各要素(タグ)を表現するための JavaScript オブジェクトのツリー構造です。ユーザーが何かアクションを取るたびに(例えば、ボタンをクリックするなど)、対応する部分の DOM が更新されます。ただし、DOM の更新は高コストな操作であり、頻繁に行うとパフォーマンスに影響を及ぼす可能性があります。

ここで、React の仮想 DOM が役立ちます。仮想 DOM は実際の DOM の複製であり、React はこの仮想 DOM を使って実際の DOM の変更を最小限に抑えることができます。これは、React が新しい状態をもとに新しい仮想 DOM ツリーを作り、それを既存の仮想 DOM ツリーと比較することで行われます。この比較を「差分検出」または「リコンシリエーション」と呼びます。

まずは、単純な React コンポーネントを作成し、その状態を更新することで仮想 DOM がどのように働くかを見てみましょう。このコンポーネントは、ボタンをクリックするとテキストが切り替わるものです。

import React, { useState } from 'react'

type Props = {
  initialText: string
  updatedText: string
}

const SimpleComponent = ({ initialText, updatedText }: Props) => {
  const [text, setText] = useState(initialText)

  const handleClick = () => {
    setText(updatedText)
  }

  return (
    <div>
      <p>{text}</p>
      <button onClick={handleClick}>Change Text</button>
    </div>
  )
}

export default SimpleComponent

このコードは、初期状態でinitialTextを表示し、ボタンをクリックするとupdatedTextにテキストが切り替わるというコンポーネントです。useStateフックを使ってtextという状態を作り、handleClick関数でその状態を更新しています。

これを起動すると、ボタンをクリックすると画面のテキストが切り替わることを確認できます。これは React(つまり仮想 DOM)が、状態の変更を検知して UI を更新する様子を示しています。

しかし、画面全体が更新されているわけではありません。React(仮想 DOM)は、状態が変更された部分だけを特定し、その部分だけを効率的に更新します。この仕組みにより、大規模な Web アプリケーションでもスムーズに動作します。

このように、React(そして仮想 DOM)は、状態の変更を効率的に画面に反映させるための強力な仕組みを提供します。この概念は、React を学ぶ上で非常に重要なので、しっかりと理解しておきましょう。

レンダリングとは?

例えば、みんなが日曜日の朝に絵を描くことが好きだとしましょう。色鉛筆やペン、クレヨンを使って、絵を描きますよね。レンダリングも、そのような絵を描く作業に似ています。

あなたがウェブサイトを開くと、コンピューター(あるいはスマホやタブレット)は、そのウェブサイトを描きます。しかし、色鉛筆やクレヨンは使わず、HTML、CSS、JavaScript という特別な言語を使います。それらの言語を使ってウェブサイトのページを描くことが「レンダリング」です。

さらに、もしウェブサイトで何かをクリックしたり、入力したりすると、コンピュータはその部分を新しく描き直すことがあります。この描き直す作業もレンダリングの一部です。それはまるで絵に何かを追加したり、一部を修正したりするようなものですね。

だから、レンダリングは、ウェブサイトがコンピュータ上で描かれる方法のことを指します。そして、それは私たちがインターネットを利用するために必要な作業なのです。

Next.js で、仮想 DOM を実装

それでは、Next.js と TypeScript を使って、仮想 DOM の動きを確認するシンプルな例を見ていきましょう。以下にpages/index.tsxでの実装例を示します。

// pages/index.tsx
import { useState } from 'react'

type ButtonProps = {
  onClick: () => void
  children: React.ReactNode
}

const Button = ({ onClick, children }: ButtonProps) => {
  console.log('Button component rendered')
  return <button onClick={onClick}>{children}</button>
}

const IndexPage = () => {
  const [count, setCount] = useState(0)
  const handleClick = () => setCount(count + 1)

  console.log('IndexPage component rendered')
  return (
    <div>
      <p>You clicked {count} times</p>
      <Button onClick={handleClick}>Click me</Button>
    </div>
  )
}

export default IndexPage

このコードでは、ButtonコンポーネントとIndexPageコンポーネントが定義されています。Buttonコンポーネントは再利用可能なボタンの役割を果たし、クリック時に親から渡されたonClick関数を実行します。IndexPageコンポーネントは、ボタンをクリックするたびにカウンターを増やす機能を持ちます。

また、各コンポーネントがレンダリングされた際にコンソールにログを出力するようにしています。この例を通じて、React がどのコンポーネントを再レンダリングするかを観察できます。ボタンをクリックすると、IndexPageコンポーネントとButtonコンポーネントの両方が再レンダリングされます。これは、親コンポーネントが再レンダリングされると、その子コンポーネントもデフォルトで再レンダリングされるためです。

仮想 DOM を活用

さらに高度なテクニックとして、React のReact.memo関数を使って、子コンポーネントの不要な再レンダリングを防ぐ方法を見てみましょう。

// pages/index.tsx
import { useState } from 'react'

type ButtonProps = {
  onClick: () => void
  children: React.ReactNode
}

const Button = React.memo(({ onClick, children }: ButtonProps) => {
  console.log('Button component rendered')
  return <button onClick={onClick}>{children}</button>
})

const IndexPage = () => {
  const [count, setCount] = useState(0)
  const handleClick = () => setCount(count + 1)

  console.log('IndexPage component rendered')
  return (
    <div>
      <p>You clicked {count} times</p>
      <Button onClick={handleClick}>Click me</Button>
    </div>
  )
}

export default IndexPage

ここでは、ButtonコンポーネントをReact.memoでラップしています。これにより、Buttonコンポーネントに渡される props が変更されない限り、React はそのコンポーネントの再レンダリングをスキップします。つまり、Buttonコンポーネントは、onClick関数やchildrenが変更されたときにのみ再レンダリングされます。

これが仮想 DOM の力です。React.memoは、仮想 DOM と差分検出アルゴリズムを活用して、必要なレンダリングのみを行い、パフォーマンスを向上させる方法を提供します。

以上が React の仮想 DOM についての基本的な理解と、Next.js と TypeScript を用いた具体的な利用例です。この力を理解し、自身のプロジェクトに活用してパフォーマンスの向上を図ることができます。

Next.js と TypeScript で、React の仮想DOMを理解する