1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

久しくReactを触っていなくて設計知識も抜けているので、デザインパターンを勉強しようと思い、Compound Components Patternについてまとめてみました。

Compound Components Pattern とは

Compound Components Pattern(複合コンポーネントパターン) とは、親コンポーネントが共有する状態や API を提供し、子コンポーネントは親からの入力(Context 経由など)で振る舞うデザインパターンです。

子は親の内部で自由に配置でき、親は子の数や構成に依存しない API を提供します。Context を使うと深いネストでも状態共有が簡単になります。

メリット

  • API がまとまり、消費側の使い勝手が良くなる
  • 子コンポーネントを自由に組み替えられる柔軟性

デメリット

  • Context の値設計を間違えると不必要な再レンダリングが起きる
  • Context は Provider の value が変わると、購読している子がまとめて再レンダリングされやすい
  • 過剰に凝った設計は可読性・再利用性を下げる

いつ使うべきか

  • 複数の子コンポーネントが同じ状態に依存する
  • 子の並びや数が柔軟で、親が API を提供したい

避けるべきか

  • 単純な一要素だけの UI(オーバーヘッドになる)

実装例

App.jsx

import React from 'react'
import { Tabs, TabList, Tab, TabPanels, TabPanel } from './Tabs'

const App = () => (
  <Tabs defaultIndex={1}>
    <TabList>
      <Tab index={0}>Tab A</Tab>
      <Tab index={1}>Tab B</Tab>
      <Tab index={2}>Tab C</Tab>
    </TabList>

    <TabPanels>
      <TabPanel index={0}>Content A</TabPanel>
      <TabPanel index={1}>Content B</TabPanel>
      <TabPanel index={2}>Content C</TabPanel>
    </TabPanels>
  </Tabs>
)

export default App

Tabs.jsx

import React, { createContext, useContext, useMemo, useState, useCallback } from 'react'

const TabsContext = createContext(null)

export const Tabs = ({ children, defaultIndex = 0, index, onChange }) => {
  const isControlled = typeof index === 'number'
  const [uncontrolledIndex, setUncontrolledIndex] = useState(defaultIndex)

  const activeIndex = isControlled ? index : uncontrolledIndex

  const onSelect = useCallback(
    (i) => {
      if (!isControlled) setUncontrolledIndex(i)
      if (onChange) onChange(i)
    },
    [isControlled, onChange]
  )

  const value = useMemo(() => ({ activeIndex, onSelect }), [activeIndex, onSelect])

  return <TabsContext.Provider value={value}>{children}</TabsContext.Provider>
}

export const useTabs = () => {
  const ctx = useContext(TabsContext)
  if (!ctx) throw new Error('Tabs components must be used within Tabs')
  return ctx
}

export const TabList = ({ children }) => <div role="tablist">{children}</div>

export const Tab = ({ children, index }) => {
  const { activeIndex, onSelect } = useTabs()
  const selected = activeIndex === index

  return (
    <button
      type="button"
      role="tab"
      aria-selected={selected}
      onClick={() => onSelect(index)}
      style={{ fontWeight: selected ? 'bold' : 'normal' }}
    >
      {children}
    </button>
  )
}

export const TabPanels = ({ children }) => <div>{children}</div>

export const TabPanel = ({ children, index }) => {
  const { activeIndex } = useTabs()
  return (
    <div role="tabpanel" hidden={activeIndex !== index}>
      {children}
    </div>
  )
}

まとめ

  • Compound Components Patternとは、親コンポーネントが共有する状態や API を提供し、子コンポーネントは親からの入力(Context 経由など)で振る舞うデザインパターン
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?