Оптимізуємо рендеринг render props в React

5 хв. читання

Поговоримо про техніку render props (і схожий з нею підхід children as a function) в React. Вона стала дуже популярною останнім часом через авторитет багатьох вельми поширених бібліотек, таких як react-router v4, formik, які якраз і базуються на цьому архітектурному рішенні.

Чому ж render props так жваво знайшли своє місце в арсеналі react-розробників? Відповідь достатньо проста: щоб зменшити кількість place-of-truth та необхідність розділяти бізнес-логіку між компонентами. Більше можна прочитати в англомовних ресурсах: Use a Render Prop!, Understanding React Render Props by Example.

Техніка render props – потужний інструмент, але і вона, на мій погляд, має слабке місце в плані оптимізації рендеринга (що є темою обговорень не одної статті по react на medium.com), що пов'язано з використанням функцій, здебільшого анонімних, а це потрібно враховувати, обираючи майбутню архітектуру вашого застосунку. Уявімо ситуацію, що ви маєте архітектурно однакові списки – переліки карток. Проте в картки в кожного списку різні за структурою та стилем. Відповідно для карток потрібно використовувати різні компоненти, а для списку достатнього одного універсального компонента, що і прийматиме render prop.

Організуємо наш тестовий приклад, використовуючи підхід render props для компонента List (точніше, children as a function), а для простоти будемо працювати лише з одним видом карток – компонентом Card.

// простий компонент для рендерингу конкретної картки 
const Card = ({ children }) => {
  console.log("Card render", children);
  return <div className="list-card">{children}</div>;
};

// компонент List з основною логікою та структурою стилів
// Він може бути використаний для рендерингу декількох списків з різними картами
const List = ({ items, children }) => {
  console.log("List render");

  if (!Array.isArray(items)) return null;

  return (
    <ul className="list">
      {items.map(el => (
        <li key={el} className="list__item">
          {children(el)}
        </li>
      ))}
    </ul>
  );
};

// Кореневий елемент додатка з логікою зміни стану
class App extends Component {
  state = {
    time: 0
  };

  handleButtonClick = () => {
    this.setState({ time: this.state.time + 1 });
  };

  render() {
    const { time } = this.state;

    return (
      <div>
        <h2>simple render props example</h2>
        <div>
          <div>Click button to change state of the App: {time}</div>
          <button onClick={this.handleButtonClick}>Click</button>
        </div>
        <List items={todoList}>{p => <Card>{p}</Card>}</List>
      </div>
    );
  }
}

Потестити приклад наживо можна на CodeSandbox

Як ви можете бачити в консолі (або вже здогадалися), усі списки та картки оновлюються після зміни стану компонента App, хоча сам контент карток та списку не змінився. Додатковий рендеринг без необхідності, не порядок. В гру вступає перше правило оптимізації в React – перевірка чи змінилися props. Це досягається через shallowEqual порівняння. Для цього перепишемо наші функціональні компоненти через PureComponent (або ж можна написати High-Order Component (HOC) – якщо ви хочете залишити функціональні компоненти. Це дозволить нам зменшити кількість рендерів.

class Card extends PureComponent {
  render() {
    const { children } = this.props;

    console.log("Card render", children);
    return <div className="list-card">{children}</div>;
  }
}

class List extends PureComponent {
  render() {
    const { items, children } = this.props;
    
    console.log("List render");
    if (!Array.isArray(items)) return null;

    return (
      <ul className="list">
        {items.map(el => (
          <li key={el} className="list__item">
            {children(el)}
          </li>
        ))}
      </ul>
    );
  }
}

Оновлений варіант можна протестувати на CodeSandbox.

Здається все має бути круто, але як можете помітити (або здогадатися), компонент List продовжує оновлюватися при зміні стану батьківського компонента. Що ж, вузьким місцем підходу render props є анонімна функція, що постійно перевизначається при кожному новому рендері. Одним з варіантів вирішення є заміна її на метод батьківського компонента (що є більш оптимальним з точки зору читабельності та логіки). Другий варіант, якщо у вас батьківський компонент функціональний i/або ви не хочете додаткову логіку до нього, – змінити умову перевірки props в shouldComponentUpdate для компонента List (виключити з порівняння render та/чи children).

Як варіант, можна модифікувати компонент List наступним чином:


class List extends Component {
  shouldComponentUpdate(nextProps) {
    return !shallowEqualsWithoutChildren(this.props, nextProps);
  }

  render() {
    ...
  }
}

}

Остаточний варіант на CodeSandbox.

А як ви оптимізуєте render props?

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація