Logo of
Published on

Implementacja compound components przy użyciu React Context

Authors
  • avatar
    Name
    Marcin Parda
    Twitter

Problem

W poprzednim artykule pokazałem jak uniknąć przekazywania dziesiątek propsów do komponentów przy użyciu wzorca Compound Components. Jednakże napotkaliśmy kilka problemów:

  • Dodaliśmy nową warstwę abstrakcji, której trzeba pamiętać i wiedzieć, że istnieje. Przydałoby się ją również udokumentować.
  • Nie wiemy dokładnie jakie komponenty powinniśmy użyć w Panel jeśli chcemy, żeby cały komponent wyglądał konsekwentnie i działał poprawnie. Przydałoby się jakieś podpowiedzenie składni, które powiedziałoby nam co powinniśmy użyć w Panel, prawda? :)
  • Jeśli chcielibyśmy, żeby komponenty mogły ze sobą komunikować się, np. po kliknięciu przycisku w PanelHeader chcielibyśmy ukryć cały komponent Panel, to musielibyśmy przekazywać dodatkowe propsy do komponentów (handleClick, isPanelVisible).

Rozwiązanie

Compound components przy użyciu React Context

Compound component to wzorzec do tworzenia wielokrotnego użytku komponentów, które składają się z wielu mniejszych komponentów.

W compound component, główny komponent nadrzędny dostarcza zestaw komponentów podrzędnych, które współpracują ze sobą w celu wdrożenia ogólnej funkcjonalności komponentu nadrzędnego.

W poprzednim artykule stworzyliśmy komponent Panel, który wyglądał tak:

<Panel title="Panel">
  <PanelHeader>
    <h3>Header</h3>
  </PanelHeader>
  <PanelBody>
    <p>Body</p>
  </PanelBody>
</Panel>

Przede wszystkim naprawmy ten problem:

  • Nie wiemy dokładnie jakie komponenty powinniśmy użyć w Panel jeśli chcemy, żeby cały komponent wyglądał konsekwentnie i działał poprawnie. Przydałoby się jakieś podpowiedzenie składni, które powiedziałoby nam co powinniśmy użyć w Panel, prawda? :)

Aby to zrobić wystarczy, że będziemy stosować taką składnię w naszym komponencie Panel:

Panel.Header = function PanelHeader({ children }: ParentProps) {
  return <header>{children}</header>
}

Dzięki temu w naszym edytorze, gdy wpiszemy Panel. i naciśniemy CTRL + Spacja, powinniśmy dostać listę wszystkich komponentów, które możemy użyć w Panel. Co pozwala nam używać go w taki sposób:

<Panel title="Panel">
  <Panel.Header>
    <h3>Header</h3>
  </Panel.Header>
  <Panel.Body>
    <p>Body</p>
  </Panel.Body>
</Panel>

Rozwiązuje to nasz pierwszy problem. Poza tym ten kod lepiej się dokumentuje. W jednym pliku mamy listę komponentów podrzędnych, które powinniśmy używać wewnątrz znaczników <Panel>. Przejdźmy do drugiego problemu:

  • Jeśli chcielibyśmy, żeby komponenty mogły ze sobą komunikować się, np. po kliknięciu przycisku w PanelHeader chcielibyśmy ukryć cały komponent Panel, to musielibyśmy przekazywać dodatkowe propsy do komponentów (handleClick, isPanelVisible).

Jak to osiągnąć bez nadmiernego dodawania propsów. Na pomoc przyjdzie nam React Context. Jeśli jeszcze nie wiesz czym jest React Context, polecam przeczytać ten artykuł z nowej dokumentacji reacta.

Na początek zdefiniujemy PanelContext dla naszego panelu:

interface PanelContextValue {
  isVisible: boolean
  showPanel: () => void
  hidePanel: () => void
}

const PanelContext = React.createContext({} as PanelContextValue)

W tym kontekście będziemy przechowywać dane o tym czy Panel jest widoczny oraz dwie funkcje, które będą zmieniały widoczność panelu.

Następną rzeczą do zrobienia będzie dodanie stanu i Provider do komponentu nadrzędnego Panel.

export const Panel = ({ title, children }: PanelProps & ParentProps) => {
  const [isVisible, toggleVisibility] = React.useState(false)
  const showPanel = () => toggleVisibility(true)
  const hidePanel = () => toggleVisibility(false)

  if (!isVisible) return <button onClick={showPanel}>Show Panel</button>

  return (
    <PanelContext.Provider value={{ isVisible, showPanel, hidePanel }}>
      <div>
        <h1>{title}</h1>
        {children}
      </div>
    </PanelContext.Provider>
  )
}

Jeśli panel nie jest widoczny zamiast tego pokażemy przycisk, który po kliknięciu pokaże panel. Ostatnią rzeczą będzie wykorzystanie wartości z Provider w komponentach podrzędnych.

Przepiszmy teraz Panel.Header tak, aby mógł ukryć cały panel bez przekazywania propsów do tego komponentu:

Panel.Header = function Header({ children }: ParentProps) {
  const { hidePanel } = useContext(PanelContext)
  return (
    <header>
      {children}
      <button onClick={hidePanel}>Hide Panel</button>
    </header>
  )
}

W ten sposób przekazujemy do nagłówka możliwość zmiany stanu komponentu Panel, pozostawiając jako jedyny propsy children. W podobny sposób możemy przekazać odpowiednią funkcjonalność do innych podkomponentów.

Ostateczny plik Panel.tsx wygląda teraz tak:

import React from 'react'

interface PanelProps {
  title: string
}

interface ParentProps {
  children: React.ReactNode
}

interface PanelContextValue {
  isVisible: boolean
  showPanel: () => void
  hidePanel: () => void
}

const PanelContext = React.createContext({} as PanelContextValue)

export const Panel = ({ title, children }: PanelProps & ParentProps) => {
  const [isVisible, toggleVisibility] = React.useState(false)
  const showPanel = () => toggleVisibility(true)
  const hidePanel = () => toggleVisibility(false)

  if (!isVisible) return <button onClick={showPanel}>Show Panel</button>

  return (
    <PanelContext.Provider value={{ isVisible, showPanel, hidePanel }}>
      <div>
        <h1>{title}</h1>
        {children}
      </div>
    </PanelContext.Provider>
  )
}

Panel.Header = function Header({ children }: ParentProps) {
  const { hidePanel } = React.useContext(PanelContext)
  return (
    <header>
      {children}
      <button onClick={hidePanel}>Hide Panel</button>
    </header>
  )
}

Panel.Body = function Body({ children }: ParentProps) {
  return <main>{children}</main>
}

A jego użycie nie zmieniło się od ostatniego fragmentu kodu mimo, że dodaliśmy nową funkcjonalność:

<Panel title="Panel">
  <Panel.Header>
    <h3>Header</h3>
  </Panel.Header>
  <Panel.Body>
    <p>Body</p>
  </Panel.Body>
</Panel>

Podsumowanie

To wszystko. Mam nadzieję, że podobał Ci się ten artykuł. Jeśli masz jakieś pytania, śmiało zadawaj je w komentarzach.

Oto dodatek dla Ciebie. Krótkie podsumowanie artykułu. Możesz go wykorzystać do tworzenia fiszek (np. w Anki).

Co to jest compound component?

Compound component Compound component to komponent, który składa się z wielu komponentów podrzędnych. Komponenty podrzędne są zwykle łączone ze sobą w celu utworzenia bardziej złożonego komponentu, który wykonuje bardziej złożoną funkcję.

Przykład:

<Panel title="Panel">
  <Panel.Header>
    <h3>Header</h3>
  </Panel.Header>
  <Panel.Body>
    <p>Body</p>
  </Panel.Body>
</Panel>

Widzisz błąd / literówkę w artykule? Zgłoś poprawkę lub dodaj komentarz na dole.

Zobacz post na GitHubie