Глибока передача даних з контекстом

Зазвичай ви передаєте інформацію від батьківського компонента до дочірнього за допомогою пропсів. Але передача пропсів може стати багатослівною і незручною, якщо вам потрібно передати їх через багато компонентів у середині, або якщо багато компонентів у вашому додатку потребують однакової інформації. Контекст дозволяє батьківському компоненту зробити деяку інформацію доступною для будь-якого компонента у дереві під ним - незалежно від глибини - без явної передачі її через пропси.

  • Що таке "буріння на опорах"
  • Як замінити повторювані передачі пропсів на контекст
  • Загальні випадки використання контексту
  • Поширені альтернативи контексту

Проблема з передачею пропсів

Передача пропсів є чудовим способом явної передачі даних через дерево інтерфейсу користувача до компонентів, які його використовують.

Але передача пропсів може стати багатослівною і незручною, якщо вам потрібно передати якийсь з пропсів глибоко по дереву, або якщо багато компонентів потребують одного і того ж самого пропсу. Найближчий спільний предок може бути далеко від компонентів, яким потрібні дані, і підняття стану вгору так високо може призвести до ситуації, яка називається "буріння пропсів".

Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in purple. The value flows down to each of the two children, both highlighted in purple.

Підняття стану вгору

Diagram with a tree of ten nodes, each node with two children or less. The root node contains a bubble representing a value highlighted in purple. The value flows down through the two children, each of which pass the value but do not contain it. The left child passes the value down to two children which are both highlighted purple. The right child of the root passes the value through to one of its two children - the right one, which is highlighted purple. That child passed the value through its single child, which passes it down to both of its two children, which are highlighted purple.

Свердління пропсів

Було б чудово, якби існував спосіб "телепортувати" дані до компонентів у дереві, які їх потребують, без передачі пропсів. Завдяки функції контексту в React така можливість є!

Контекст: альтернатива передачі пропсів

Контекст дозволяє батьківському компоненту надавати дані всьому дереву під ним. Існує багато способів використання контексту. Ось один із прикладів. Розглянемо компонент Heading, який приймає рівень для свого розміру:

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Heading level={2}>Heading</Heading>
      <Heading level={3}>Sub-heading</Heading>
      <Heading level={4}>Sub-sub-heading</Heading>
      <Heading level={5}>Sub-sub-sub-heading</Heading>
      <Heading level={6}>Sub-sub-sub-sub-heading</Heading>
    </Section>
  );
}
export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}
export default function Heading({ level, children }) {
  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
.section {
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
  border: 1px solid #aaa;
}

Припустимо, ви хочете, щоб кілька заголовків у межах одного Section завжди мали однаковий розмір:

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Section>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Section>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Section>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}
export default function Heading({ level, children }) {
  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
.section {
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
  border: 1px solid #aaa;
}

Наразі ви передаєте проп рівня кожному <Heading> окремо:

<Section>
  <Heading level={3}>About</Heading>
  <Heading level={3}>Photos</Heading>
  <Heading level={3}>Videos</Heading>
</Section>

Було б добре, якби ви могли передати проп level до компонента <Section> і вилучити його з <Heading>. Таким чином, ви зможете забезпечити однаковий розмір усіх заголовків в одному розділі:

<Section level={3}>
  <Heading>About</Heading>
  <Heading>Photos</Heading>
  <Heading>Videos</Heading>
</Section>

Але як компонент <Heading> може дізнатися про рівень свого найближчого <Section>? Для цього потрібно, щоб дочірній компонент "запитував" дані звідкись зверху у дереві.

Ви не можете зробити це лише за допомогою пропсів. Тут у гру вступає контекст. Ви зробите це у три кроки:

  1. Створіть контекст. (Ви можете назвати його LevelContext, оскільки він призначений для рівня заголовка)
  2. .
  3. Використовуйте той контекст з компонента, якому потрібні дані. (Heading використовуватиме LevelContext).
  4. Надайте той контекст з компонента, який визначає дані. (Section надасть LevelContext.)

Контекст дозволяє батькові, навіть віддаленому, надавати певні дані всьому дереву під ним.

Diagram with a tree of three components. The parent contains a bubble representing a value highlighted in orange which projects down to the two children, each highlighted in orange.

Використання контексту у близьких нащадках

Diagram with a tree of ten nodes, each node with two children or less. The root parent node contains a bubble representing a value highlighted in orange. The value projects down directly to four leaves and one intermediate component in the tree, which are all highlighted in orange. None of the other intermediate components are highlighted.

Використання контексту у віддалених дочірніх елементах

Крок 1: Створення контексту

Перш за все, вам потрібно створити контекст. Вам потрібно буде експортувати його з файлу, щоб ваші компоненти могли його використовувати:

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading level={1}>Title</Heading>
      <Section>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Heading level={2}>Heading</Heading>
        <Section>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Heading level={3}>Sub-heading</Heading>
          <Section>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
            <Heading level={4}>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}
export default function Heading({ level, children }) {
  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
import { createContext } from 'react';

export const LevelContext = createContext(1);
.section {
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
  border: 1px solid #aaa;
}

Єдиним аргументом для createContext є значення за замовчуванням. Тут 1 позначає найбільший рівень заголовка, але ви можете передати будь-яке значення (навіть об'єкт). Ви побачите значення значення за замовчуванням на наступному кроці.

Крок 2: Використання контексту

Імпортуйте хук useContext з React та вашого контексту:

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

Наразі компонент Heading зчитує рівень з пропсів:

export default function Heading({ level, children }) {
  // ...
}

Натомість вилучіть проп level і прочитайте значення з контексту, який ви щойно імпортували, LevelContext:

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

useContext - це хук. Подібно до useState та useReducer, ви можете викликати хук безпосередньо всередині React-компонента (не всередині циклів або умов). useContext повідомляє React, що компонент Heading хоче прочитати LevelContext.

Тепер, коли компонент Heading не має пропусків level, вам більше не потрібно передавати проп level до Heading у вашому JSX таким чином:

<Section>
  <Heading level={4}>Sub-sub-heading</Heading>
  <Heading level={4}>Sub-sub-heading</Heading>
  <Heading level={4}>Sub-sub-heading</Heading>
</Section>

Оновіть JSX так, щоб замість нього отримував Section:

<Section level={4}>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
  <Heading>Sub-sub-heading</Heading>
</Section>

Нагадуємо, що це розмітка, яку ви намагалися змусити працювати:

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section level={1}>
      <Heading>Title</Heading>
      <Section level={2}>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section level={3}>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section level={4}>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
import { createContext } from 'react';

export const LevelContext = createContext(1);
.section {
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
  border: 1px solid #aaa;
}

Зверніть увагу, що цей приклад ще не зовсім працює! Всі заголовки мають однаковий розмір, тому що хоч ви і використовуєте контекст, ви ще не надали його.React не знає, де його взяти!

Якщо ви не вкажете контекст, React використає значення за замовчуванням, яке ви вказали на попередньому кроці. У цьому прикладі ви вказали 1 як аргумент до createContext, тому useContext(LevelContext) поверне 1, встановивши всі ці заголовки як <h1>. Вирішимо цю проблему, створивши для кожного Section власний контекст.

Крок 3: Надання контексту

Компонент Section наразі рендерить свої дочірні елементи:

export default function Section({ children }) {
  return (
    <section className="section">
      {children}
    </section>
  );
}

Обгорніть їх постачальником контексту, щоб надати їм LevelContext:

import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

Це повідомляє React: "якщо будь-який компонент всередині цього <Section> запитує LevelContext, надайте йому цей рівень". Компонент використовуватиме значення найближчого <LevelContext.Provider> у дереві інтерфейсу користувача над ним.

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section level={1}>
      <Heading>Title</Heading>
      <Section level={2}>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section level={3}>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section level={4}>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  switch (level) {
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
import { createContext } from 'react';

export const LevelContext = createContext(1);
.section {
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
  border: 1px solid #aaa;
}

Це той самий результат, що і в оригінальному коді, але вам не потрібно передавати проп level кожному компоненту Heading! Замість цього він "з'ясовує" рівень свого заголовка, запитуючи найближчий Section вище:

  1. Ви передаєте проп рівня до <Section>.
  2. Section загортає своїх нащадків у <LevelContext.Provider value={level}>.
  3. Heading запитує найближче значення LevelContext вище з useContext(LevelContext).

Використання та надання контексту з одного й того самого компонента

Наразі вам все ще доводиться вказувати рівень кожної секції вручну:

export default function Page() {
  return (
    <Section level={1}>
      ...
      <Section level={2}>
        ...
        <Section level={3}>
          ...

Оскільки контекст дозволяє читати інформацію з компонента вище, кожен Section може прочитати рівень з Section вище, і автоматично передати level + 1 вниз. Ось як це можна зробити:

import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

З цією зміною вам не потрібно передавати проп рівня ані до <Section>, ані до <Heading>:

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children }) {
  const level = useContext(LevelContext);
  return (
    <section className="section">
      <LevelContext.Provider value={level + 1}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  switch (level) {
    case 0:
      throw Error('Heading must be inside a Section!');
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
import { createContext } from 'react';

export const LevelContext = createContext(0);
.section {
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
  border: 1px solid #aaa;
}

Тепер і Heading, і Section прочитають LevelContext, щоб зрозуміти, наскільки вони "глибокі". А Section обгортає своїх нащадків у LevelContext, щоб вказати, що все всередині нього знаходиться на "глибшому" рівні.

У цьому прикладі використано рівні заголовків, оскільки вони візуально показують, як вкладені компоненти можуть перевизначати контекст. Але контекст корисний і для багатьох інших випадків використання. Ви можете передати будь-яку інформацію, необхідну для всього піддерева: поточну колірну тему, користувача, який увійшов до системи, і так далі.

Контекст проходить через проміжні компоненти

Ви можете вставити скільки завгодно компонентів між компонентом, який надає контекст, і тим, який його використовує. Це стосується як вбудованих компонентів, таких як <div>, так і компонентів, які ви можете створити самостійно.

У цьому прикладі той самий компонент Post (з пунктирною рамкою) показано на двох різних рівнях вкладеності. Зверніть увагу, що <Heading> всередині нього отримує свій рівень автоматично від найближчого <Section>:

import Heading from './Heading.js';
import Section from './Section.js';

export default function ProfilePage() {
  return (
    <Section>
      <Heading>My Profile</Heading>
      <Post
        title="Hello traveller!"
        body="Read about my adventures."
      />
      <AllPosts />
    </Section>
  );
}

function AllPosts() {
  return (
    <Section>
      <Heading>Posts</Heading>
      <RecentPosts />
    </Section>
  );
}

function RecentPosts() {
  return (
    <Section>
      <Heading>Recent Posts</Heading>
      <Post
        title="Flavors of Lisbon"
        body="...those pastéis de nata!"
      />
      <Post
        title="Buenos Aires in the rhythm of tango"
        body="I loved it!"
      />
    </Section>
  );
}

function Post({ title, body }) {
  return (
    <Section isFancy={true}>
      <Heading>
        {title}
      </Heading>
      <p><i>{body}</i></p>
    </Section>
  );
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Section({ children, isFancy }) {
  const level = useContext(LevelContext);
  return (
    <section className={
      'section ' +
      (isFancy ? 'fancy' : '')
    }>
      <LevelContext.Provider value={level + 1}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';

export default function Heading({ children }) {
  const level = useContext(LevelContext);
  switch (level) {
    case 0:
      throw Error('Heading must be inside a Section!');
    case 1:
      return <h1>{children}</h1>;
    case 2:
      return <h2>{children}</h2>;
    case 3:
      return <h3>{children}</h3>;
    case 4:
      return <h4>{children}</h4>;
    case 5:
      return <h5>{children}</h5>;
    case 6:
      return <h6>{children}</h6>;
    default:
      throw Error('Unknown level: ' + level);
  }
}
import { createContext } from 'react';

export const LevelContext = createContext(0);
.section {
  padding: 10px;
  margin: 5px;
  border-radius: 5px;
  border: 1px solid #aaa;
}

.fancy {
  border: 4px dashed pink;
}

Ви не зробили нічого особливого, щоб це працювало. Section визначає контекст для дерева всередині нього, тому ви можете вставити <Heading> будь-де, і воно матиме правильний розмір. Спробуйте це у пісочниці вище!

Контекст дозволяє писати компоненти, які "адаптуються до свого оточення" і відображаються по-різному залежно від того, де (або, іншими словами, в якому контексті) вони рендеряться.

Те, як працює контекст, може нагадати вам успадкування властивостей CSS. У CSS ви можете вказати color: blue для <div>, і будь-який DOM-вузол всередині нього, незалежно від глибини, успадкує цей колір, якщо тільки якийсь інший DOM-вузол в середині не перевизначить його за допомогою color: green. Аналогічно, у React єдиний спосіб перевизначити якийсь вміст, що надходить зверху, - це обгорнути дочірні елементи у провайдера контексту з іншим значенням.

У CSS різні властивості, такі як color та background-color не перевизначають одна одну. Ви можете встановити для всіх <div> color червоний колір, не впливаючи на background-color. Аналогічно, різні контексти React не перевизначають один одного. Кожен контекст, який ви створюєте за допомогою createContext(), є повністю відокремленим від інших і пов'язує компоненти, що використовують та надають саме цей контекст. Один компонент може без проблем використовувати або надавати багато різних контекстів.

Перед використанням контексту

Контекст дуже спокусливий у використанні! Однак це також означає, що ним дуже легко зловживати. Той факт, що вам потрібно передати деякі пропси на кілька рівнів углиб, не означає, що вам слід поміщати цю інформацію у контекст.

Перед використанням контексту слід розглянути декілька альтернатив:

  1. Почніть з передачі пропсів. Якщо ваші компоненти не є тривіальними, немає нічого незвичайного в тому, що вам доведеться передавати десяток пропсів через десяток компонентів. Це може виглядати як рутина, але це робить дуже зрозумілим, які компоненти використовують які дані! Людина, яка супроводжує ваш код, буде рада, що ви зробили потік даних явним за допомогою пропсів.
  2. Витягти компоненти і передавати JSX як дочірні до них. Якщо ви передаєте деякі дані через багато шарів проміжних компонентів, які не використовують ці дані (а лише передають їх далі), це часто означає, що ви забули витягти деякі компоненти на шляху. Наприклад, можливо, ви передали пропси даних типу posts візуальним компонентам, які не використовують їх безпосередньо, наприклад, <Layout posts={posts} />. Замість цього, зробіть так, щоб Layout використовував children як проп, а відображав <Layout><Posts posts={posts} /></Layout>. Це зменшує кількість шарів між компонентом, що визначає дані, і компонентом, який їх потребує.

Якщо жоден з цих підходів вам не підходить, розгляньте контекст.

Використовуйте відмінки для контексту

  • Тема: Якщо ваш застосунок дозволяє користувачеві змінювати його вигляд (наприклад, темний режим), ви можете розмістити контекстний провайдер у верхній частині вашого застосунку і використовувати його у компонентах, які потребують налаштування візуального вигляду.
  • Поточний обліковий запис: Багатьом компонентам може знадобитися інформація про поточного користувача. Помістивши його у контекст, ви зможете прочитати його у будь-якому місці дерева. Деякі програми також дозволяють працювати з кількома обліковими записами водночас (наприклад, щоб залишити коментар від імені іншого користувача). У таких випадках може бути зручно обгорнути частину інтерфейсу користувача у вкладений провайдер з іншим значенням поточного облікового запису.
  • Маршрутизація: Більшість рішень для маршрутизації використовують внутрішній контекст для збереження поточного маршруту. Таким чином кожне посилання "знає", активне воно чи ні. Якщо ви збираєте власний маршрутизатор, вам також варто це зробити.
  • Керування станом: У міру зростання вашої програми ви можете мати багато станів ближче до вершини програми. Багато віддалених компонентів, розташованих нижче, можуть захотіти його змінити. Зазвичай застосовують редуктор разом із контекстом для керування складним станом і його передачі віддаленим компонентам без зайвого клопоту.

Контекст не обмежується статичними значеннями. Якщо ви передасте інше значення при наступному рендерингу, React оновить всі компоненти, що читають його нижче! Ось чому контекст часто використовується у поєднанні зі станом.

Загалом, якщо деяка інформація потрібна віддаленим компонентам у різних частинах дерева, це є гарною ознакою того, що контекст вам допоможе.

  • Контекст дозволяє компоненту надавати деяку інформацію всьому дереву під ним.
  • Передача контексту:
    1. Створіть та експортуйте його за допомогою export const MyContext = createContext(defaultValue).
    2. Передайте його хуку useContext(MyContext) для читання у будь-якому дочірньому компоненті, незалежно від глибини.
    3. Загорніть дочірні елементи у <MyContext.Provider value={...}>, щоб отримати їх від батька.
  • Контекст проходить через будь-які проміжні компоненти.
  • Контекст дозволяє писати компоненти, які "адаптуються до свого оточення".
  • Перш ніж використовувати контекст, спробуйте передати пропси або JSX як children.

Замінити свердління реквізиту контекстом

У цьому прикладі перемикання прапорця змінює проп imageSize, що передається до кожного <PlaceImage>. Стан прапорця зберігається у компоненті верхнього рівня App, але кожен <PlaceImage> повинен знати про нього.

Наразі App передає imageSize до List, який передає його кожному Place, який в свою чергу передає його до PlaceImage. Видаліть проп imageSize, і натомість передайте його з компонента App безпосередньо до PlaceImage.

Ви можете оголосити контекст у Context.js.

import { useState } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';

export default function App() {
  const [isLarge, setIsLarge] = useState(false);
  const imageSize = isLarge ? 150 : 100;
  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={isLarge}
          onChange={e => {
            setIsLarge(e.target.checked);
          }}
        />
        Use large images
      </label>
      <hr />
      <List imageSize={imageSize} />
    </>
  )
}

function List({ imageSize }) {
  const listItems = places.map(place =>
    <li key={place.id}>
      <Place
        place={place}
        imageSize={imageSize}
      />
    </li>
  );
  return <ul>{listItems}</ul>;
}

function Place({ place, imageSize }) {
  return (
    <>
      <PlaceImage
        place={place}
        imageSize={imageSize}
      />
      <p>
        <b>{place.name}</b>
        {': ' + place.description}
      </p>
    </>
  );
}

function PlaceImage({ place, imageSize }) {
  return (
    <img
      src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}
</pre>
<pre><code data-meta="src/data.js" class="language-js">export const places = [{
  id: 0,
  name: 'Bo-Kaap in Cape Town, South Africa',
  description: 'The tradition of choosing bright colors for houses began in the late 20th century.',
  imageId: 'K9HVAGH'
}, {
  id: 1, 
  name: 'Rainbow Village in Taichung, Taiwan',
  description: 'To save the houses from demolition, Huang Yung-Fu, a local resident, painted all 1,200 of them in 1924.',
  imageId: '9EAYZrt'
}, {
  id: 2, 
  name: 'Macromural de Pachuca, Mexico',
  description: 'One of the largest murals in the world covering homes in a hillside neighborhood.',
  imageId: 'DgXHVwu'
}, {
  id: 3, 
  name: 'Selarón Staircase in Rio de Janeiro, Brazil',
  description: 'This landmark was created by Jorge Selarón, a Chilean-born artist, as a "tribute to the Brazilian people."',
  imageId: 'aeO3rpI'
}, {
  id: 4, 
  name: 'Burano, Italy',
  description: 'The houses are painted following a specific color system dating back to 16th century.',
  imageId: 'kxsph5C'
}, {
  id: 5, 
  name: 'Chefchaouen, Marocco',
  description: 'There are a few theories on why the houses are painted blue, including that the color repels mosquitos or that it symbolizes sky and heaven.',
  imageId: 'rTqKo46'
}, {
  id: 6,
  name: 'Gamcheon Culture Village in Busan, South Korea',
  description: 'In 2009, the village was converted into a cultural hub by painting the houses and featuring exhibitions and art installations.',
  imageId: 'ZfQOOzf'
}];
export function getImageUrl(place) {
  return (
    'https://i.imgur.com/' +
    place.imageId +
    'l.jpg'
  );
}
ul { list-style-type: none; padding: 0px 10px; }
li { 
  margin-bottom: 10px; 
  display: grid; 
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: center;
}

Видаліть проп imageSize з усіх компонентів.

Створіть та експортуйте ImageSizeContext з Context.js. Потім обгорніть список у <ImageSizeContext.Provider value={imageSize}>, щоб передати значення вниз, і useContext(ImageSizeContext), щоб прочитати його у PlaceImage:

import { useState, useContext } from 'react';
import { places } from './data.js';
import { getImageUrl } from './utils.js';
import { ImageSizeContext } from './Context.js';

export default function App() {
  const [isLarge, setIsLarge] = useState(false);
  const imageSize = isLarge ? 150 : 100;
  return (
    <ImageSizeContext.Provider
      value={imageSize}
    >
      <label>
        <input
          type="checkbox"
          checked={isLarge}
          onChange={e => {
            setIsLarge(e.target.checked);
          }}
        />
        Use large images
      </label>
      <hr />
      <List />
    </ImageSizeContext.Provider>
  )
}

function List() {
  const listItems = places.map(place =>
    <li key={place.id}>
      <Place place={place} />
    </li>
  );
  return <ul>{listItems}</ul>;
}

function Place({ place }) {
  return (
    <>
      <PlaceImage place={place} />
      <p>
        <b>{place.name}</b>
        {': ' + place.description}
      </p>
    </>
  );
}

function PlaceImage({ place }) {
  const imageSize = useContext(ImageSizeContext);
  return (
    <img
      src={getImageUrl(place)}
      alt={place.name}
      width={imageSize}
      height={imageSize}
    />
  );
}
import { createContext } from 'react';

export const ImageSizeContext = createContext(500);
export const places = [{
  id: 0,
  name: 'Bo-Kaap in Cape Town, South Africa',
  description: 'The tradition of choosing bright colors for houses began in the late 20th century.',
  imageId: 'K9HVAGH'
}, {
  id: 1, 
  name: 'Rainbow Village in Taichung, Taiwan',
  description: 'To save the houses from demolition, Huang Yung-Fu, a local resident, painted all 1,200 of them in 1924.',
  imageId: '9EAYZrt'
}, {
  id: 2, 
  name: 'Macromural de Pachuca, Mexico',
  description: 'One of the largest murals in the world covering homes in a hillside neighborhood.',
  imageId: 'DgXHVwu'
}, {
  id: 3, 
  name: 'Selarón Staircase in Rio de Janeiro, Brazil',
  description: 'This landmark was created by Jorge Selarón, a Chilean-born artist, as a "tribute to the Brazilian people".',
  imageId: 'aeO3rpI'
}, {
  id: 4, 
  name: 'Burano, Italy',
  description: 'The houses are painted following a specific color system dating back to 16th century.',
  imageId: 'kxsph5C'
}, {
  id: 5, 
  name: 'Chefchaouen, Marocco',
  description: 'There are a few theories on why the houses are painted blue, including that the color repels mosquitos or that it symbolizes sky and heaven.',
  imageId: 'rTqKo46'
}, {
  id: 6,
  name: 'Gamcheon Culture Village in Busan, South Korea',
  description: 'In 2009, the village was converted into a cultural hub by painting the houses and featuring exhibitions and art installations.',
  imageId: 'ZfQOOzf'
}];
export function getImageUrl(place) {
  return (
    'https://i.imgur.com/' +
    place.imageId +
    'l.jpg'
  );
}
ul { list-style-type: none; padding: 0px 10px; }
li { 
  margin-bottom: 10px; 
  display: grid; 
  grid-template-columns: auto 1fr;
  gap: 20px;
  align-items: center;
}

Зверніть увагу, що компонентам у середині більше не потрібно передавати imageSize.