React 19 ベータ版がnpm
で利用できるようになりましたね。
従来のWeb開発はかつて複雑でしたが、Reactが登場したことで、プロセスは大幅に簡素化されました。再利用可能なコンポーネントと広範なエコシステムのおかげで、非常に使いやすくなっています。
Reactのエコシステムは、さまざまな開発要件を満たすために使用されるツールから、異なる種類の問題を解決するのに役立つツールまで、幅広い範囲のツールを提供しています。Reactのデザインパターンは、典型的な開発問題に対する迅速かつ信頼性の高い解決策です。
React.jsを利用する開発会社によって活用される多くのデザインパターンがあり、それぞれが開発プロジェクトでユニークな目的を果たします。
この記事では、React開発者が知っておくと「便利な」デザインパターンについて説明します。
しかし、この話題へ移る前に、Reactのデザインパターンが何であるか、そしてそれがアプリ開発にどのように役立つかを知ることが大切です。
React デザイン パターンってなぁに?
Reactデザインパターンは、開発者が一般的なソフトウェア開発プロセス中に直面する一般的な問題の解決策です。これらは再利用可能であり、アプリのコードのサイズを削減するのに役立ちます。Reactデザインパターンを使用する場合、コンポーネントロジックを共有するために複製プロセスを使用する必要はありません。
ソフトウェア開発プロジェクトに取り組んでいると、様々な複雑な問題が発生します。しかし、デザインパターンの信頼できるソリューションを使用すると、それらの複雑さを排除することができ、開発プロセスを簡素化できるようになります。これにより、可読性といった部分も向上でき、読みやすいコードも書くことができます。
アプリ開発でReactデザインパターンを使用する利点
React開発の有効性について疑問があるのは、 Reactデザインパターンの利点をまだ理解していない可能性があります。利点を知ることで、React開発をより効果的かつ効率的にすることでしょう。
再利用性
再利用可能なコンポーネントを構築できる再利用可能なテンプレートを提供します。したがって、これらの再利用可能なコンポーネントを使用してアプリケーションを開発すると、時間と労力を大幅に節約できます。さらに重要なのは、新しいプロジェクトを開始するたびにReactアプリケーションを最初から構築する必要がないことです。
拡張性
デザインパターンを使用すると、体系的にReactプログラムを作成できます。これにより、アプリのコンポーネントがよりシンプルになります。そのため、大規模なアプリケーションに取り組んでいる場合でも、メンテナンスと拡張性が簡単になります。すべてのコンポーネントが独立しているため、他のコンポーネントに影響を与えることなく、1つのコンポーネントを変更することが可能になります。
保守性
デザインパターンは、プログラミングへの体系的なアプローチを提供するため、典型的な開発問題の解決策と呼ばれます。コーディングが簡単になるため、コードベースの開発だけでなく、メンテナンスにも役立ちます。これは、大規模なReactプロジェクトに取り組んでいる場合にも当てはまります。
Reactデザインパターンを使用すると、コードがより分離され、モジュール化され、問題も分割されます。コードの小さな塊として扱う場合、コードの変更と保守が簡単になります。コードの1つのセクションを変更しても、モジュールやアーキテクチャの他の部分には影響しないため、必ず満足のいく結果が得られるでしょう。つまり、モジュール化により保守性が向上します。
効率性
Reactはコンポーネントベースの設計ですが、Virtual DOM
のおかげで、読み込み時間の短縮と迅速な更新を実現できます。アーキテクチャの不可欠な側面として、Virtual DOM
はアプリケーションの全体的な効率を向上させることを目的としています。これは、ユーザーエクスペリエンスの向上にも役立ちます。
さらに、メモ化などのデザインパターンにより、コストのかかるレンダリングの結果が保存されるため、不必要な再レンダリングを行う必要がなくなります。再レンダリングには時間がかかりますが、結果がすでにキャッシュされている場合は、要求に応じてすぐに配信できます。これはアプリのパフォーマンスの向上に役立ちます。
柔軟性
コンポーネントベースの設計のため、Reactアプリに変更を適用すると便利です。このアプローチを使用すると、コンポーネントのさまざまな組み合わせを試して独自のソリューションを構築できます。コンポーネント指向設計パターンを使用すると、適切なユーザーインターフェイスを作成することもできます。
他の有名なWeb開発フレームワークとは異なり、Reactは特定のガイドラインに従うことや意見を押し付けることはありません。これにより、開発者は自分の創造性を表現し、React開発でさまざまなアプローチや方法論を組み合わせて試すための十分な機会が得られます。
一貫性
Reactデザインパターンに従うと、アプリケーションに一貫した外観が提供され、使いやすくなります。一貫性はより良いユーザーエクスペリエンスを提供するのに役立ちますが、シンプルさによりユーザーはアプリ内を簡単に移動できるため、ユーザーエンゲージメントが向上します。これらはどちらも収益を増やすための重要な要素です。
開発者が知っておくべき React の便利なデザインパターン
デザインパターンは、開発プロジェクト中に発生する問題や課題を解決するのに役立ちます。 Reactエコシステムでは非常に多くの効率的な設計パターンが利用できるため、それらすべてを1つの投稿に含めることは非常に困難です。ただし、この記事では、一般的で効果的な便利な Reactデザインパターンについて説明します。
今回はよく業務で利用されるReactとTypeScriptでの参考事例も挙げています。
react@18.3.2
typescript@5.4.5
コンテナ・プレゼンテーションパターン
コンテナーとプレゼンテーションのパターンを使用すると、Reactコンポーネントを簡単に再利用できます。この設計パターンでは、ロジックに基づいてコンポーネントが2つの異なるセクションに分割されるためです。1つ目はビジネスロジックを含むコンテナコンポーネントであり、2 つ目はプレゼンテーションロジックで構成されるプレゼンテーションコンポーネントです。
ここでは、コンテナコンポーネントはデータをフェッチし、必要な情報を取得する役割を果たします。
一方、プレゼンテーションコンポーネントは、取得したデータをユーザーインターフェイスにレンダリングする役割を果たします。
コンテナ・プレゼンテーションパターンの例
import React, { useEffect, useState } from 'react'
import { UserList } from '@components/UserList'
import { User } from '@type/index'
export const UsersContainer = () => {
const [users, setUsers] = useState<User[]>([])
const [isLoading, setIsLoading] = useState<boolean>(false)
const [isError, setIsError] = useState<boolean>(false)
const getUsers = async () => {
setIsLoading(true)
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const data = (await response.json()) as User[]
setIsLoading(false)
if (!data) return
setUsers(data)
} catch (err) {
setIsError(true)
}
}
useEffect(() => {
getUsers()
}, [])
return <UserList isLoading={isLoading} isError={isError} users={users} />
}
// ユーザーの一覧を表示する
import React from 'react'
import { UserListProps } from '@type/index'
export const UserList = ({ isLoading, isError, users }: UserListProps) => {
if (isLoading && !isError) return <div>ローディング中...</div>
if (!isLoading && isError) {
return <>エラーが発生しました。読み込むことができません。</>
}
if (!users) return null
return (
<>
<h2>ユーザー一覧</h2>
<ul>
{users.map((user) => (
<li key={user.id}>
名前: {user.name} (メール: {user.email})
</li>
))}
</ul>
</>
)
}
フックパターン(フックを含むコンポーネント構成)
2019年に初めて導入されたフックは、React16.8
からの登場により人気を博しました。フックは、コンポーネントの要件を満たすように設計された基本機能です。機能コンポーネントが状態およびReactコンポーネントのライフサイクルメソッドにアクセスできるように使用されます。ステート(useState
)、エフェクト(useEffect
)、カスタムフック(useHooks
)は、フックの例の一部です。
コンポーネントでフックを使用すると、コードをモジュール化してテストしやすくすることができます。フックをコンポーネントに緩く結び付けることで、コードを個別にテストできます。
フックを使用したコンポーネント構成の例
// ユーザーを取得するカスタムフック
import { useEffect, useState } from 'react'
import { User, UserListProps } from '@type/index'
export const useFetchUsers = (): UserListProps => {
const [users, setUsers] = useState<User[]>([])
const [isLoading, setIsLoading] = useState<boolean>(false)
const [isError, setIsError] = useState<boolean>(false)
const controller = new AbortController()
const getUsers = async () => {
setIsLoading(true)
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users',
{
headers: {
'Content-Type': 'application/json',
},
}
)
const data = (await response.json()) as User[]
setIsLoading(false)
if (!data) return
setUsers(data)
} catch (err) {
setIsError(true)
}
}
useEffect(() => {
getUsers()
return () => {
controller.abort()
}
}, [])
return { users, isLoading, isError }
}
ここで、このカスタムフックをUsersContainer
コンポーネントで使用するためにインポートする必要があります。
import React from 'react'
import { UserList } from '@components/UserList'
import { useFetchUsers } from '@hooks/useFetchUsers'
export const UsersContainer = () => {
const { users, isLoading, isError } = useFetchUsers()
return <UserList isLoading={isLoading} isError={isError} users={users} />
}
レデューサーパターン
複雑なロジックに依存するさまざまな状態を持つ複雑なReactアプリケーションに取り組んでいる場合は、カスタム状態ロジックと初期状態値を備えた状態レデューサー設計パターンを利用することをお勧めします。ここでの値はnull
またはオブジェクトのいずれかになります。
Reactで状態レデューサーデザインパターンを使用する場合、コンポーネントの状態を変更する代わりに、Reducer関数が渡されます。 Reducer関数を受け取ると、コンポーネントは現在の状態でアクションを実行します。そのアクションに基づいて、新しい状態を返します。
アクションは、type
プロパティを持つオブジェクトで構成されます。type
プロパティは、実行する必要があるアクションを記述するか、そのアクションを実行するために必要な追加のアセットについて言及します。
カウンターのレデューサーパターンのコード例
import React, { useReducer } from 'react'
import { State, Action } from '@type/index'
const initialState = {
count: 0,
}
const reducer = (state: State, action: Action) => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 }
case 'decrement':
return { ...state, count: state.count - 1 }
default:
return state
}
}
export const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
<p>カウント: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>増加</button>
<button onClick={() => dispatch({ type: 'decrement' })}>減少</button>
</div>
)
}
プロバイダー パターン
アプリケーションがツリー内のネストされたコンポーネントへのプロップの送信またはプロップのドリルを停止するようにしたい場合は、プロバイダーのデザインパターンを使用してそれを実現できます。Reactの Context API はこのパターンを提供できます。
import { createContext } from 'react'
export const UserContext = createContext('初期値ユーザー')
import React, { useContext } from 'react'
import { UserContext } from '@contexts/UserContext'
export const Dashboard = () => {
const userValue = useContext(UserContext)
return <h1>{userValue}</h1>
}
import React from 'react'
import { UserContext } from '@contexts/UserContext'
import { Dashboard } from '@components/Dashboard'
const App = () => {
return (
<div>
<UserContext.Provider value="Reactユーザー">
<Dashboard />
</UserContext.Provider>
</div>
)
}
export default App
上記のプロバイダーパターンのコードは、コンテキストを使用して新しく作成されたオブジェクトにprops
を直接渡す方法を示しています。状態のプロバイダーとコンシューマーの両方をコンテキストに含める必要があります。上記のコードでは、UserContext
を使用するダッシュボードコンポーネントがコンシューマーであり、アプリコンポーネントがプロバイダーです。
より理解を深めるために、以下の図式を確認してください。
プロバイダーパターンを使用しない場合は、コンポーネントB
とC
が中間コンポーネントとして機能するプロップドリルを通じて、コンポーネントA
からコンポーネントD
にprops
を渡す必要があります。ただし、プロバイダーパターンを使用すると、プロップをA
からD
に直接送信できます。
HOC(高階)パターン
アプリケーション全体でコンポーネントロジックを再利用したい場合は、高度な機能を備えたデザインパターンが必要です。データの取得、ロギング、認証などのさまざまなタイプの機能が付属しています。
HOCは、JavaScript関数である React機能コンポーネントの構成的な性質に基づいて構築されています。したがって、React API と混同してはいけません。
アプリケーション内の高階コンポーネントは、JavaScript の高階関数と同様の性質を持ちます。これらの関数は純粋な順序であり、副作用はありません。また、JavaScriptの高階関数と同様に、HOCはデコレータ関数としても機能します。
高階のReactコンポーネントの構造例
import React from 'react'
import { MessageProps } from '@type/index'
export const Message = ({ message }: MessageProps) => {
return <>{message}</>
}
import React, { ComponentType } from 'react'
import { MessageProps } from '@type/index'
export const WithUpperCase = (
WrappedComponent: ComponentType<MessageProps>
) => {
return (props: MessageProps) => {
const { message } = props
const upperCaseMessage = message.toUpperCase()
return <WrappedComponent {...props} message={upperCaseMessage} />
}
}
import React from 'react'
import { WithUpperCase } from '@components/WithUpperCase'
import { Message } from '@components/Message'
export const Enhanced = () => {
const EnhancedMessage = WithUpperCase(Message)
return (
<div>
<p>
<EnhancedMessage message="hello world" />
</p>
<p>
<EnhancedMessage message="my name is taro yamada." />
</p>
</div>
)
}
複合パターン
連携して相互に補完する関連パーツの集合を複合コンポーネントと呼びます。多くの要素を含むモーダルコンポーネントは、そのようなデザインパターンの単純な例です。
Modalコンポーネントによって提供される機能は、トリガー、コンテンツ、モーダルなどの要素の共同作業の結果です。
import React, { Children, cloneElement, useState } from 'react'
import { ModalProps } from '@type/index'
const Modal = ({ children }: { children: JSX.Element[] }) => {
const [isOpen, setIsOpen] = useState<boolean>(false)
const toggleModal = () => {
setIsOpen(!isOpen)
}
return (
<div>
{Children.map(children, (child) =>
cloneElement(child, { isOpen, toggleModal })
)}
</div>
)
}
const ModalTrigger = ({ toggleModal, children }: ModalProps) => (
<button onClick={toggleModal}>{children}</button>
)
const ModalContent = ({ isOpen, toggleModal, children }: ModalProps) =>
isOpen && (
<div className="modal">
<div className="modal-content">
<span className="close" onClick={toggleModal}>
×
</span>
{children}
</div>
</div>
)
export const CardModal = () => (
<Modal>
<ModalTrigger isOpen={false} toggleModal={() => {}}>
<p>モーダルを開く</p>
</ModalTrigger>
<ModalContent isOpen={false} toggleModal={() => {}}>
<>
<h2>モーダルコンテンツ</h2>
<p>シンプルな内容です</p>
</>
</ModalContent>
</Modal>
)
複合コンポーネントは、さまざまなコンポーネント間の接続を表現できるAPIも提供します。
レンダープロップパターン
レンダープロップパターンは、コンポーネントが他のコンポーネントと関数をprop
として共有できるようにするメソッドです。これは、ロジックの繰り返しに関連する問題の解決に役立ちます。受信側のコンポーネントは、このプロップを呼び出し、戻り値を使用してコンテンツをレンダリングできます。
レンダープロップは子プロップを使用してコンポーネントに関数を渡すため、子プロップとも呼ばれます。
さまざまなコンポーネントが特定の機能を必要とする場合、Reactアプリのコンポーネントにその機能や要件を含めることは困難です。このような問題のある状況をクロスカッティングと呼びます。
レンダープロップパターンは、関数をプロップとして子コンポーネントに渡します。親コンポーネントも、子コンポーネントと同じロジックと状態を共有します。これは、コードの重複を防ぐ懸念の分離を達成するのに役立ちます。
メソッドを利用すると、ユーザーコンポーネントを単独で管理できるコンポーネントを構築できます。この設計パターンは、そのロジックをReactアプリケーションの他のコンポーネントとも共有します。
開発者は異なるコンポーネントに対して同じコードを書き直す必要がなくなります。
トグルメソッドの切り替えコード例
import { Component } from 'react'
import { ToggleProps, ToggleState } from '@type/index'
export class ToggleMethod extends Component<ToggleProps, ToggleState> {
constructor(props: ToggleProps) {
super(props)
this.state = { on: false }
}
toggle = () => {
this.setState((state) => ({ on: !state.on }))
}
render() {
return this.props.render({
on: this.state.on,
toggle: this.toggle,
})
}
}
import React, { Component } from 'react'
import { ToggleMethod } from '@functions/ToggleMethod'
export class Toggle extends Component {
render() {
return (
<div>
<h1>切り替え</h1>
<ToggleMethod
render={({ on, toggle }) => (
<div>
<button onClick={toggle}>{on ? 'オフ' : 'オン'}</button>
<p>切り替えは {on ? 'オン' : 'オフ'}.</p>
</div>
)}
/>
</div>
)
}
}
まとめ
今回、デザインパターンを調査する上で使用したコードはgithub上に置いておきました。少しでもお役になれば幸いです。
今回、この記事で説明したReactのデザインパターンは、開発プロジェクト中に最も広く使用されているものの一部分です。このようなデザインパターンを活用して、Reactライブラリの可能性を最大限に引き出すことができます。十分に理解し、効果的・効率的に取り入れることをお勧めします。再利用性が高く、保守性も優れている Reactアプリケーションを構築するのに役立ちます。