Reactの3つのデザインパターン:HOC と Hooks と Compound Pattern

English follows Japanese. 英語の文章は後半にあります。

こんにちは。主にフロントエンド周りの開発を担当しているフンです。

React.jsは、今やフロントエンド開発では欠かせないライブラリです。UIを効率よく構築するための多彩なツールが揃っていますが、実際のプロジェクトでは「コードがスパゲッティ化してしまった」「どこから手をつければいいのか分からない」なんて悩んだことはありませんか?

そんなときに力を発揮するのがデザインパターンです。デザインパターンとは、開発者が直面しがちな課題に対して効果的に対処するための、いわば「お墨付きの設計パターン」です。これを活用すれば、再利用性の高いコードが書けるだけでなく、チームでの開発もぐっとスムーズになります。

そこで今回は、React開発者ならぜひ知っておきたい 3つのデザインパターン を取り上げ、それぞれの利点と具体的な実装例を解説します。

Higher-Order Components (HOC)

Reactで開発をしていると、「このロジック、あっちのコンポーネントでも使いたい…」と思うことはありませんか?たとえば、特定のスタイリングを共通化したい場合や、認証が必要なページを複数作成したいとき、あるいはグローバルな状態管理を行いたいときなどです。

こうした状況で役に立つのが、Higher-Order Component (HOC) というデザインパターンです。

HOCは、一言でいえば「他のコンポーネントをラップして、新しい機能を追加するための特殊な関数」です。渡されたコンポーネントに共通のロジックを注入し、拡張された新しいコンポーネントを返します。このため、似たようなロジックを何度も書き直すことなく、どのコンポーネントでも簡単に再利用できるのです。

例:HOCを用いたログ機能

import React from 'react';

// Higher-Order Component
const withLogger = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log('Props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

// Regular Component
const HelloWorld = ({ name }) => {
  return <h1>Hello, {name}!</h1>;
};

// Enhanced Component with Logger
const HelloWorldWithLogger = withLogger(HelloWorld);

// Usage
const App = () => <HelloWorldWithLogger name="John" />;
export default App;

withLoggerはプロップをログに記録するHOCです。これにより、元のコンポーネントを変更することなく、ログ機能を追加できます。

Hooks

React開発をしているなら、一度は Hooks という言葉を耳にしたことがあるはずです。これはただの新機能ではありません。Hooksは、関数コンポーネントをパワーアップし、React開発を一変させた革新的な仕組みです。
React 16.8で導入されて以来、今や現代のReactアプリ開発に欠かせない基本ツールとなりました。

簡単に言えば、Hooksは関数コンポーネントでも状態管理や副作用の処理を可能にする「魔法の関数」です。それまで、Reactの状態管理やライフサイクルの処理はクラスコンポーネントに依存していました。しかし、Hooksの登場により、わずかなコードで同じ機能を実装できるようになったのです。

たとえば、以前はこうした状態管理やロジックはクラスベースのコンポーネントでなければ書けませんでしたが、今ではuseStateuseEffectといったHooksを使うだけで、直感的に状態管理や副作用の処理が行えます。

例:データ取得のためのCustom Hook

import React, { useState, useEffect } from 'react';

// Custom Hook
const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then((response) => response.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, [url]);

  return { data, loading };
};

// Component using the Custom Hook
const DataFetcher = ({ url }) => {
  const { data, loading } = useFetch(url);

  if (loading) {
    return <p>Loading...</p>;
  }

  return <div>{JSON.stringify(data)}</div>;
};

// Usage
const App = () => <DataFetcher url="https://api.example.com/data" />;
export default App;

この例では、useFetchは、URLからデータを取得するロジックをカプセル化しています。DataFetcherコンポーネントはこのフックを使用してデータを取得して表示し、コードをよりクリーンで再利用可能にしています。

Hooksの利点

React Hooksは、ただ「便利」なだけではなく、Reactアプリの構造自体を大きく改善できる可能性を秘めています。ここでは、Hooksを使うことで得られる主なメリットを分かりやすく整理してみました。

1. コード行数の削減で、見通しがクリアに!

Hooksを導入すると、関心事ごとにコードをまとめられるため、コードベースが驚くほどシンプルになります。クラスコンポーネントで何十行も必要だった状態管理やライフサイクルメソッドの記述が、Hooksを使えば数行で完結することも。これにより、読みやすく、保守しやすいコードを保ちながら、開発効率も向上します。

2. 複雑なコンポーネントをスッキリ簡素化

クラスコンポーネントを使ったことがある方なら、「あれ?バインディングがうまくいかない…」といった経験をしたことがあるのではないでしょうか?クラスベースでは、状態管理が煩雑になりやすく、リファクタリングやホットリロードで思わぬトラブルが発生しがちです。

React Hooksを使えば、こうした問題を解消し、関数型プログラミングのシンプルさを享受できます。また、Hooksはクラスと比べてミニファイ(コードの圧縮)にも向いているため、パフォーマンスの向上にも貢献します。

3. 状態ロジックの再利用が超簡単に

Reactのクラスコンポーネントでは、状態やロジックを再利用するために、HOC(Higher-Order Components)やRender Propsなどのテクニックが必要でした。しかし、これらのパターンはコードが読みにくくなるというデメリットも伴います。

Hooksでは、Custom Hookとして状態ロジックを簡単に切り出し、どのコンポーネントからも再利用できるようになります。これにより、同じロジックを何度も書き直す手間が省け、コードの一貫性が保たれるため、バグの発生率も減少します。

4. 見えないロジックの共有も簡単に

Hooksは、見た目には表示されない非視覚的ロジック(状態管理やAPI呼び出しなど)をカプセル化し、シンプルなJavaScript関数として扱えるようにします。これにより、Reactの特定の機能に縛られず、どんなコンポーネントにも自由に状態やロジックを注入できます。

例えば、カスタムフックを使って認証ロジックを切り出せば、どのページでもuseAuthと呼ぶだけで、ユーザーの認証状態を確認できるようになります。これにより、チーム内でロジックを共有しやすく、再利用性も格段に向上します。

React Hooksは、コードの簡潔化、複雑なコンポーネントの整理、ロジックの再利用といった多くの利点を提供します。まだクラスコンポーネントを使っている方も、ぜひこの機会にHooksを試してみてください。あなたのReact開発が、よりスマートで効率的なものになるはずです!

Hooksの注意点

React Hooksは非常に便利ですが、実は「使いこなすのが難しい…」と感じることも多いのではないでしょうか?その理由は、Hooksに特有のルールや、誤った使い方によるパフォーマンスの問題などがあるためです。ここでは、React Hooksを使うときに気をつけたい3つのポイントを解説します。

1. Hooksの「ルール」を守るべし

React Hooksには、いくつかの厳格なルールがあります。たとえば、「Hooksは関数のトップレベルで呼び出さなければならない」「ループや条件分岐内でHooksを使ってはいけない」などです。これらのルールを破ると、思わぬバグが発生したり、アプリの動作が不安定になることがあります。

解決策:ESLintプラグインを使おう!

こうしたルール違反を避けるために、eslint-plugin-react-hooksという専用のESLintプラグインを導入しましょう。このプラグインを使えば、ルールを守れていない箇所を自動的に警告してくれます。特に大規模なチーム開発では、コードレビューの負担を減らし、ミスを未然に防ぐ効果が期待できます。

2. useEffect は理解するまでが大変

Hooksを使い始めたとき、誰もが一度は頭を抱えるのがuseEffectの扱いです。依存配列(dependency array)に何を指定すべきかを間違えると、意図しないタイミングで副作用が発生したり、無限ループに陥ることさえあります。

解決策:まずは「依存配列」の意味を理解しよう

useEffectの依存配列は「この配列内の値が変更されたときにだけuseEffectを再実行する」というトリガーの役割を持っています。これを理解したうえで、必要な値だけを依存配列に含めるようにするのがポイントです。

3. useCallback と useMemo の「やりすぎ」に注意!

useCallbackuseMemo は、パフォーマンス最適化のために導入されましたが、むやみに使うとかえって逆効果になることもあります。これらのHooksは、コンポーネントの再レンダリングを防ぐために関数や値をメモ化(キャッシュ)しますが、過度に使用するとコードが読みづらくなり、バグの原因になることも。

解決策:「本当に必要か?」を考える

useCallbackuseMemo を使う前に、「これを使わなくてもパフォーマンスに大きな影響はないか?」を一度考えてみましょう。最適化すること自体が目的になってしまうと、結果としてパフォーマンスが悪化したり、コードの可読性が下がることになります。最適化は、問題が発生してから取り組むのでも遅くはありません。

Hooksは非常にパワフルなツールですが、正しく使いこなすには時間がかかります。最初は慎重に、そしてルールを守りながら、少しずつ使い方に慣れていきましょう。これらの注意点をしっかりと理解しておくことで、バグの少ない、信頼性の高いReactアプリケーションを構築できるようになるはずです!

Compound Pattern

Reactでアプリケーションを開発していると、親コンポーネントと子コンポーネントの間で状態や機能を共有し、密接に連携する必要が出てくることがあります。こうした複雑なUIを構築するときに役立つのが、Compound Patternです。

Compound Patternとは、親と子のコンポーネントが一体となって機能することを前提に設計するパターンです。たとえば、タブ(Tabs)やモーダル(Modal)など、複数のコンポーネントが協調して動作するUI要素において特に効果を発揮します。

このパターンを使うと、親コンポーネントが全体の状態を管理し、子コンポーネントはその状態を参照しながら、独立して動作することが可能になります。結果として、柔軟性と拡張性の高いUIを構築できるため、アプリケーションの複雑化を防ぎつつ、使い勝手の良い構造を保てます。

例:Compound Patternを用いたタブコンポーネント

import React, { useState, createContext, useContext } from 'react';

// Create Context
const TabsContext = createContext();

const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div>{children}</div>
    </TabsContext.Provider>
  );
};

const TabList = ({ children }) => {
  return <div className="tab-list">{children}</div>;
};

const Tab = ({ children, index }) => {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button
      className={`tab ${activeTab === index ? 'active' : ''}`}
      onClick={() => setActiveTab(index)}
    >
      {children}
    </button>
  );
};

const TabPanels = ({ children }) => {
  const { activeTab } = useContext(TabsContext);
  return <div className="tab-panels">{children[activeTab]}</div>;
};

const TabPanel = ({ children }) => {
  return <div className="tab-panel">{children}</div>;
};

// Usage
const App = () => (
  <Tabs>
    <TabList>
      <Tab index={0}>Tab 1</Tab>
      <Tab index={1}>Tab 2</Tab>
      <Tab index={2}>Tab 3</Tab>
    </TabList>
    <TabPanels>
      <TabPanel>Content 1</TabPanel>
      <TabPanel>Content 2</TabPanel>
      <TabPanel>Content 3</TabPanel>
    </TabPanels>
  </Tabs>
);

export default App;

この例では、TabsTabListTabTabPanels、およびTabPanelコンポーネントが協調して動作し、React Contextを使用してアクティブなタブの状態を管理しています。このパターンにより、複雑なUIコンポーネントの構成と管理が容易になります。

Compound Patternの利点

Compound Patternを使うと、複数のコンポーネントが互いに協調しながら機能するため、複雑なUIを扱う際に大きなメリットを発揮します。ここでは、Compound Patternを導入することで得られる2つの主要な利点を紹介します。

1. 状態管理がシンプルになる

Compound Patternを使うと、親コンポーネントが内部状態を一元管理し、その状態を子コンポーネント間で簡単に共有できるようになります。これにより、各子コンポーネントが独自に状態を持つ必要がなく、状態の更新やデータの整合性をシンプルに保つことが可能です。

たとえば、タブ(Tabs)やアコーディオン(Accordion)など、複数の子コンポーネントが一緒に動作するUIでは、親コンポーネントがアクティブなタブや開かれているセクションを一括管理します。こうすることで、子コンポーネント同士が直接やり取りする必要がなくなり、バグの発生リスクを減らせるだけでなく、全体の挙動もわかりやすくなります。

2. インポートがシンプルで直感的

Compound Patternを使うと、親コンポーネントをインポートするだけで、その中で使用される子コンポーネントをすべて暗黙的に使用可能になります。これにより、インポート時にいちいち複数の子コンポーネントを指定する必要がなくなり、コードの可読性が向上します。

Compound Patternの欠点

Compound Patternは、柔軟で協調的なUIを作成するのに最適なパターンですが、その構造ゆえにいくつかの欠点も存在します。特に、コンポーネントのネストプロパティの管理に関する問題が発生することがあるため、使い方を誤ると予期せぬバグに悩まされることがあります。ここでは、Compound Patternを使用する際に注意すべき2つの落とし穴を紹介します。

1. コンポーネントのネスト制限:柔軟性が失われることも

Compound Patternを使う際、親コンポーネントと子コンポーネントの間でデータや状態を渡すのにReact.Children.mapを使うことがよくあります。これにより、親コンポーネントが直接の子に対してデータを渡せるようになるのですが、子コンポーネントが他のコンポーネントでラップされている場合に問題が発生します。

つまり、React.Children.map親コンポーネントの直接の子にしか作用しないため、ネスト構造が複雑になったり、別のコンポーネントで子コンポーネントをラップしたい場合に、親が子に意図したプロパティ(例:opentoggle など)を渡せなくなってしまうのです。

解決策:React Context APIを検討しよう

この問題を回避するためには、状態やデータの伝達にReact Context APIを使用するのが効果的です。Contextを使えば、どのコンポーネントツリー内でも状態を自由に参照できるため、ネスト構造に関わらず、必要なプロパティを子コンポーネントに渡せます。

2. React.cloneElementによるプロパティの衝突

Compound Patternでは、子コンポーネントにプロパティを渡す際にReact.cloneElementを使用することがよくあります。React.cloneElementを使うと、既存の要素を複製しながら新しいプロパティを上書きすることができます。しかし、ここで問題となるのが、同じ名前のプロパティが競合するケースです。

例えば、cloneElementを使ってonClickプロパティを上書きする場合、元々のonClickと新しいonClickが競合し、意図した挙動にならないことがあります。React.cloneElementはデフォルトで浅いマージ(shallow merge)を行うため、名前が衝突すると元のプロパティが失われてしまうのです。

解決策:プロパティの意図的なマージ

こうした競合を避けるには、プロパティを上書きするのではなく、マージして組み合わせることを意識しましょう。

まとめ

Reactで堅牢なアプリケーションを構築するためには、デザインパターンの理解と活用が欠かせません。Higher-Order Components (HOC)Hooks、そしてCompound Patternは、クリーンで再利用性の高いコードを書くための強力なツールです。これらを使いこなすことで、コードベースが明確になり、管理が容易になるだけでなく、アプリケーションの保守性とスケーラビリティも格段に向上します。

各パターンには得意とする役割があります。共通ロジックを整理したいときはHOC、ロジックを分離して管理しやすくしたいときはHooks、複雑なUIの構造をシンプルに保ちたいときはCompound Patternを適用するのが効果的です。

これらのパターンを組み合わせることで、より堅牢でモジュール化されたアプリケーションを構築でき、将来的な機能追加や変更にも柔軟に対応できる設計を実現できます。React開発の次のステップとして、ぜひこれらのデザインパターンを取り入れ、ワンランク上のアプリケーションを目指してみましょう!

 

(フン)


Design Patterns in React.js

React.js, a widely-used JavaScript library for building user interfaces, provides numerous ways to efficiently manage and reuse code. Design patterns are proven solutions to common problems that developers encounter, and they help in maintaining a clean and structured codebase. In this blog post, we’ll delve into three crucial design patterns in React: Higher-Order Components (HOCs), Hooks, and the Compound Pattern. We’ll explore their advantages and provide examples to demonstrate their implementation.

Higher-Order Components (HOC)

import React from 'react';

// Higher-Order Component
const withLogger = (WrappedComponent) => {
  return class extends React.Component {
    componentDidMount() {
      console.log('Props:', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
};

// Regular Component
const HelloWorld = ({ name }) => {
  return <h1>Hello, {name}!</h1>;
};

// Enhanced Component with Logger
const HelloWorldWithLogger = withLogger(HelloWorld);

// Usage
const App = () => <HelloWorldWithLogger name="John" />;
export default App;

In this example, withLogger is an HOC that logs the props passed to the WrappedComponent (HelloWorld). This allows you to add logging functionality without modifying the original component.

Hooks

Hooks are functions that let you use state and other React features in functional components. They were introduced in React 16.8 and have become a fundamental part of modern React development. Although Hooks are not necessarily a design pattern, Hooks play a very important role in your application design. Many traditional design patterns can be replaced by Hooks.

Example: Custom Hook for Fetching Data

import React, { useState, useEffect } from 'react';

// Custom Hook
const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then((response) => response.json())
      .then((data) => {
        setData(data);
        setLoading(false);
      });
  }, [url]);

  return { data, loading };
};

// Component using the Custom Hook
const DataFetcher = ({ url }) => {
  const { data, loading } = useFetch(url);

  if (loading) {
    return <p>Loading...</p>;
  }

  return <div>{JSON.stringify(data)}</div>;
};

// Usage
const App = () => <DataFetcher url="https://api.example.com/data" />;
export default App;

The useFetch custom hook encapsulates the logic for fetching data from a URL. The DataFetcher component uses this hook to fetch and display data, making the code cleaner and more reusable.

Pros

Fewer lines of code

Hooks allows your group code by concern and functionality, and not by lifecycle. This makes the code not only cleaner and concise but also shorter. Below is a comparison of a simple stateful component of a searchable product data table using React, and how it looks in Hooks after using the useState keyword.

Stateful components

Simplifies complex components

JavaScript classes can be difficult to manage, hard to use with hot reloading and may not minify as well. React Hooks solves these problems and ensures functional programming is made easy. With the implementation of Hooks, We don’t need to have class components.

Reusing stateful logic

Classes in JavaScript encourage multiple levels of inheritance that quickly increase overall complexity and potential for errors. However, Hooks allow you to use state, and other React features without writing a class. With React, you can always reuse stateful logic without the need to rewrite the code over and over again. This reduces the chances of errors and allows for composition with plain functions.

Sharing non-visual logic

Until the implementation of Hooks, React had no way of extracting and sharing non-visual logic. This eventually led to more complexities, such as the HOC patterns and Render props, just to solve a common problem. But, the introduction of Hooks has solved this problem because it allows for the extraction of stateful logic to a simple JavaScript function.

There are of course some potential downsides to Hooks worth keeping in mind:

Have to respect its rules, without a linter plugin, it is difficult to know which rule has been broken. Need considerable time practicing to use properly (Exp: useEffect). Be aware of the wrong use (Exp: useCallback, useMemo).

Compound Pattern

The Compound Pattern allows you to create components that work together in a coordinated way. It’s useful for building complex components where child components need to communicate with a parent component.

Example: Tabs Component with Compound Pattern

import React, { useState, createContext, useContext } from 'react';

// Create Context
const TabsContext = createContext();

const Tabs = ({ children }) => {
  const [activeTab, setActiveTab] = useState(0);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div>{children}</div>
    </TabsContext.Provider>
  );
};

const TabList = ({ children }) => {
  return <div className="tab-list">{children}</div>;
};

const Tab = ({ children, index }) => {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  return (
    <button
      className={`tab ${activeTab === index ? 'active' : ''}`}
      onClick={() => setActiveTab(index)}
    >
      {children}
    </button>
  );
};

const TabPanels = ({ children }) => {
  const { activeTab } = useContext(TabsContext);
  return <div className="tab-panels">{children[activeTab]}</div>;
};

const TabPanel = ({ children }) => {
  return <div className="tab-panel">{children}</div>;
};

// Usage
const App = () => (
  <Tabs>
    <TabList>
      <Tab index={0}>Tab 1</Tab>
      <Tab index={1}>Tab 2</Tab>
      <Tab index={2}>Tab 3</Tab>
    </TabList>
    <TabPanels>
      <TabPanel>Content 1</TabPanel>
      <TabPanel>Content 2</TabPanel>
      <TabPanel>Content 3</TabPanel>
    </TabPanels>
  </Tabs>
);

export default App;

In this example, the Tabs, TabList, Tab, TabPanels, and TabPanel components work together using React Context to manage the state of the active tab. This pattern makes it easy to compose and manage complex UI components.

Pros

Compound components manage their own internal state, which they share among the several child components. When implementing a compound component, we don’t have to worry about managing the state ourselves.

When importing a compound component, we don’t have to explicitly import the child components that are available on that component.

Cons

When using the React.Children.map to provide the values, the component nesting is limited. Only direct children of the parent component will have access to the open and toggle props, meaning we can’t wrap any of these components in another component.

Cloning an element with React.cloneElement performs a shallow merge. Already existing props will be merged together with the new props that we pass. This could end up in a naming collision, if an already existing prop has the same name as the props we’re passing to the React.cloneElement method. As the props are shallowly merged, the value of that prop will be overwritten with the latest value that we pass.

Understanding and applying design patterns in React can significantly improve the maintainability and scalability of your code. Higher-Order Components (HOC), Hooks, and the Compound Pattern are powerful tools in your React toolkit, enabling you to write cleaner, more reusable, and more expressive code. By leveraging these patterns, you can create more robust and modular applications.

 

(Hung)

採用情報

株式会社シェアウィズでは、新しいメンバーの募集を積極的に行っています。フルタイムの勤務から学生インターンまで、ご関心をお持ちの方はお気軽にコンタクトしてください!

採用ページへ

開発
ShareWis Blog(シェアウィズ ブログ)
タイトルとURLをコピーしました