Provider Pattern

Довольно часто нам необходимо, чтобы данные были доступны многим (если не всем) компонентам в нашем приложении. И хотя мы можем передавать данные через props, это может значительно усложнить разработку при большом количестве компонентов.

Если у нас есть большое количество вложенных компонентов, то мы столкнемся с так называемым props drilling, когда нам придется передавать данные по всей цепочке компонентов. Поддержка такого подхода становится практически невозможной, т.к. проследить откуда приходят данные становится проблематично.

Допустим, у нас есть компонент App, содержащий некоторые данные, а глубоко в дереве компонентов находятся компоненты ListItem, Header и Text, которым нужны эти данные. Для получения этих данных нам было нужно передавать их через пропсы, через все промежуточные компоненты.

Это бы выглядело примерно так:

function App() {
  const data = { ... }

  return (
    <div>
      <SideBar data={data} />
      <Content data={data} />
    </div>
  )
}

const SideBar = ({ data }) => <List data={data} />
const List = ({ data }) => <ListItem data={data} />
const ListItem = ({ data }) => <span>{data.listItem}</span>

const Content = ({ data }) => (
  <div>
    <Header data={data} />
    <Block data={data} />
  </div>
)
const Header = ({ data }) => <div>{data.title}</div>
const Block = ({ data }) => <Text data={data} />
const Text = ({ data }) => <h1>{data.text}</h1>

Передавать данные через props таким способом довольно затруднительно. Если мы захотим переименовать проп data в будущем, нам будет нужно переименовать его во всех компонентах. И чем больше становится наше приложение, тем хуже станет ситуация с проп дриллингом.

Оптимальным решением будет исключить все компоненты, которым не нужны данные. Нам нужно что-то, что даст компонентам, нуждающимся в данных, прямой доступ к ним, без привязки к prop drilling.

И здесь паттерн провайдера – Provider Pattern поможет нам! С помощью Provider Pattern, мы обеспечим доступность данных множеству компонентов. Вместо передачи данных на каждый следующий уровень через пропсы, мы обернем все компоненты в Provider. Provider это компонент высшего порядка – High Order Component, который предоставлен объектом Context. Мы можем создать объект Context, использую метод Реакта createContext.

Provider получает проп value, который содержит все данные, которые мы хотим передать. Все компоненты, обернутые в этот провайдер, смогут получить доступ к значению value.

const DataContext = React.createContext()

function App() {
  const data = { ... }

  return (
    <div>
      <DataContext.Provider value={data}>
        <SideBar />
        <Content />
      </DataContext.Provider>
    </div>
  )
}

Нам больше не нужно вручную передавать data prop в каждый компонент. Итак, как ListItem, Header и Text смогут получить доступ к data?

Каждый компонент может получить доступ к data, используя хук useContext. Этот хук получает контекст к которому привязан data, в данном случае DataContext. Хук useContext позволяет нам считывать и записывать данные в объект контекста.

const DataContext = React.createContext();

function App() {
  const data = { ... }

  return (
    <DataContext.Provider value={data}>
      <SideBar />
      <Content />
    </DataContext.Provider>
  )
}

const SideBar = () => <List />
const List = () => <ListItem />
const Content = () => <div><Header /><Block /></div>


function ListItem() {
  const { data } = React.useContext(DataContext);
  return <span>{data.listItem}</span>;
}

function Text() {
  const { data } = React.useContext(DataContext);
  return <h1>{data.text}</h1>;
}

function Header() {
  const { data } = React.useContext(DataContext);
  return <div>{data.title}</div>;
}

Компоненты, не нуждающиеся в data, теперь не имеют к ней доступ. Нам больше не нужно беспокоиться о передаче пропсов через несколько уровней компонентов, которым эти данные даже не нужны.

Provider pattern очень удобен в случаях, когда нам нужно хранить какие-то глобальным данные. Например, его можно использовать для хранения состояния темы UI для множества компонентов.

import React from "react";
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);

Мы хотим, чтобы пользователь мог переключать дневной и ночной режимы. Вместо передачи значения текущей темы каждому компоненту, мы обернем эти компоненты в провайдер ThemeContext и передадим текущую тему провайдеру.

export const ThemeContext = React.createContext();

const themes = {
  light: {
    background: "#fff",
    color: "#000"
  },
  dark: {
    background: "#171717",
    color: "#fff"
  }
};

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  const providerValue = {
    theme: themes[theme],
    toggleTheme
  };

  return (
    <div className={`App theme-${theme}`}>
      <ThemeContext.Provider value={providerValue}>
        <Toggle />
        <List />
      </ThemeContext.Provider>
    </div>
  );
}

Так как компоненты Toggle и List обернуты в провайдер ThemeContext, мы имеем доступ к значениям theme и toggleTheme, которые передали в value провайдера.

Внутри компонента Toggle мы можем использовать функцию toggleTheme для обновления темы.

import React, { useContext } from "react";
import { ThemeContext } from "./App";

export default function Toggle() {
  const theme = useContext(ThemeContext);

  return (
    <label className="switch">
      <input type="checkbox" onClick={theme.toggleTheme} />
      <span className="slider round" />
    </label>
  );
}

Компоненту List совершенно нет дела до значения нашей темы. Однако ListItem может напрямую получить доступ к theme.

import React, { useContext } from "react";
import { ThemeContext } from "./App";

export default function TextBox() {
  const theme = useContext(ThemeContext);

  return <li style={theme.theme}>...</li>;
}

Отлично, мы передали данные о теме только тем компонентам, которым они были нужны.

import React, { useState } from "react";
import "./styles.css";

import List from "./List";
import Toggle from "./Toggle";

export const themes = {
  light: {
    background: "#fff",
    color: "#000"
  },
  dark: {
    background: "#171717",
    color: "#fff"
  }
};

export const ThemeContext = React.createContext();

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  return (
    <div className={`App theme-${theme}`}>
      <ThemeContext.Provider value={{ theme: themes[theme], toggleTheme }}>
        <>
          <Toggle />
          <List />
        </>
      </ThemeContext.Provider>
    </div>
  );
}

Хуки

Мы можем создать хук для передачи контекста в компоненты. Вместо импорта useContext и контекста в каждый компонент, мы можем использовать хук, который будет возвращать нужный нам контекст.

function useThemeContext() {
  const theme = useContext(ThemeContext);
  return theme;
}

Чтобы убедиться, что это тема правильная, мы будем выбрасывать ошибку, если useContext(ThemeContext) возвращает false.

function useThemeContext() {
  const theme = useContext(ThemeContext);
  if (!theme) {
    throw new Error("useThemeContext must be used within ThemeProvider");
  }
  return theme;

Вместо оборачивания компонентов в ThemeContext.Provider мы создадим HOC, который обернет компонент и даст доступ к контексту. Таким образом мы отделим логику контекста от компонентов отображения, что позволит переиспользовать провайдер в дальнейшем.

function ThemeProvider({children}) {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  const providerValue = {
    theme: themes[theme],
    toggleTheme
  };

  return (
    <ThemeContext.Provider value={providerValue}>
      {children}
    </ThemeContext.Provider>
  );
}

export default function App() {
  return (
    <div className={`App theme-${theme}`}>
      <ThemeProvider>
        <Toggle />
        <List />
      </ThemeProvider>
    </div>
  );
}

Теперь каждый компонент, которому нужен доступ к ThemeContext может просто использовать хук useThemeContext.

export default function TextBox() {
  const theme = useThemeContext();

  return <li style={theme.theme}>...</li>;
}

Создавая хуки для различных контекстов, мы разделяем логику провайдеров от компонентов отображения.

Где используется

Некоторые библиотеки предоставляют встроенные провайдеры, значения которых мы можем использовать в нужных компонентах. Хороший пример это styled-components.

Библиотека styled-components предоставляет нам ThemeProvider. Каждый styled компонент получает доступ к значению этого провайдера и вместо создания собственного Context API мы можем просто использовать этот провайдер.

Давайте посмотрим простой пример List и обернем компоненты в ThemeProvider, импортированный из библиотеки styled-components.

import { ThemeProvider } from "styled-components";

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  return (
    <div className={`App theme-${theme}`}>
      <ThemeProvider theme={themes[theme]}>
        <>
          <Toggle toggleTheme={toggleTheme} />
          <List />
        </>
      </ThemeProvider>
    </div>
  );
}

Вместо передачи пропса style в компонент ListItem, мы создадим компонент styled.li. И так как это styled компонент, мы сможем получить доступ к значению theme.

import styled from "styled-components";

export default function ListItem() {
  return (
    <Li>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
      commodo consequat.
    </Li>
  );
}

const Li = styled.li`
  ${({ theme }) => `
     background-color: ${theme.backgroundColor};
     color: ${theme.color};
  `}
`;

Отлично, теперь мы можем легко применять стили к styles components с помощью ThemeProvider.

import React, { useState } from "react";
import { ThemeProvider } from "styled-components";
import "./styles.css";

import List from "./List";
import Toggle from "./Toggle";

export const themes = {
  light: {
    background: "#fff",
    color: "#000"
  },
  dark: {
    background: "#171717",
    color: "#fff"
  }
};

export default function App() {
  const [theme, setTheme] = useState("dark");

  function toggleTheme() {
    setTheme(theme === "light" ? "dark" : "light");
  }

  return (
    <div className={`App theme-${theme}`}>
      <ThemeProvider theme={themes[theme]}>
        <>
          <Toggle toggleTheme={toggleTheme} />
          <List />
        </>
      </ThemeProvider>
    </div>
  );
}

Преимущества

Provider pattern/Context API делает возможным доступ к данным во множестве компонентов, без ручной передачи через несколько слоев компонентов.

Этот паттерн уменьшает риск случайного появления ошибок при рефакторинге. Ранее, если мы хотели переименовать проп, нам было нужно сделать это всем приложении, где этот проп использовался.

Мы больше не столкнемся с prop drilling, который значительно усложнял понимание потока данных в приложении, когда не всегда понятно, откуда приходят пропсы. Provider Pattern передает данные только тем компонентам, которым эти данные действительно нужны.

Так же мы можем создать некое подобие глобального стейта и компоненты смогут получить к нему доступ.

Минусы

В некоторых случаях использование провайдера может привести к проблемам с производительностью. Все компоненты, которые получают данные от провайдера будут делать ререндер при изменении состояния. Чтобы избежать этого, можно создать несколько провайдеров для каждого отдельного сценария.

  •  
  •  
  •  
  •  
  •  
  •