ОБНОВЛЕНИЕ 09/2021:

С 2017 года все немного изменилось - в наши дни вы можете легко добавлять функции состояния и жизненного цикла с помощью React Hooks, поэтому методы, описанные здесь, могут больше не иметь отношения к вам.

Однако есть некоторые необычные функции жизненного цикла, такие как getSnapshotBeforeUpdate, getDerivedStateFromError и componentDidCatch, которые еще НЕ имеют эквивалента хуков, и в этом случае эта статья может пригодиться.

Кроме того, это может быть забавное чтение и / или вдохновение, так что все равно прочтите его!

Мир React похож на фильм ужасов: с одной стороны, у нас есть красивые компоненты с состоянием, которые бегают без единой заботы в своей жизни (-циклы), в то время как их братья без гражданства, зомби этого странного мира, обречены бродить по миру. поля всегда жаждут чего-то, чего они никогда не получат: brai… я имею в виду, состояние!

Но в отличие от большинства фильмов ужасов, мы могли получить счастливый конец для наших друзей-нежити - не только предоставив им состояние, но и вдохнув в них жизнь (циклы).

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

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

Итак, с этого момента, давайте начнем!

«Рюуга waga teki wo kurau»

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

Для тех, кто не «в курсе», позвольте мне рассказать вам:

Стрелочные функции имеют 2 преимущества по сравнению с обычными функциями (помимо более прохладного имени - если вы мне не верите, просто добавьте «стрелку» к случайному слову, и это сделает его примерно в 10 раз круче. «Стрелка свинья» - см. ?):

  1. Они менее многословны:
// regular function
function () {
  return someStuff;
}
// arrow function
() => (someStuff)

2. У них нет своего собственного «этого», но они выводят его из своей лексической области.

(Для получения более подробной информации посетите http://exploringjs.com/es6/ch_arrow-functions.html)

Но как это связано с React?

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

class MyComponent extends React.Component {
  render() {
    return (
      <div>
        ... // More stuff
      </div>
    );
  }
}

вместо этого мы можем выразить это как:

const MyComponent = () => (
  <div>
    ... // More stuff
  </div>
);

Меньше набора текста и, возможно, легче рассуждать - что в этом не нравится?

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

(Да, вы правильно прочитали, «свои» методы жизненного цикла. Несмотря на то, что ESLint ругает вас за то, что вы не используете компоненты без состояния, когда это возможно, React все равно внутренне конвертирует компоненты без состояния в обычные. React Fiber обещает повышение производительности в будущем , но на данный момент, похоже, на самом деле нет никакой реальной разницы.)

Итак, возникает вопрос: как мы можем сохранить зомби-вид и добавить методы состояния и жизненного цикла к компоненту без состояния?

Компоненты высшего порядка спешат на помощь!

Компоненты более высокого порядка (= HOC) - это функции, которые принимают компонент в качестве входных данных и возвращают новый компонент, который отображает исходный компонент. По сути, они превращают ваш исходный компонент в другой компонент.

Это полезно для добавления или изменения свойств, функциональности или других аспектов оригинала. Если вы использовали Redux, вы, вероятно, уже использовали «connect», который является компонентом более высокого порядка.

(Для получения дополнительной информации см. Https://facebook.github.io/react/docs/higher-order-components.html)

По-настоящему простой HOC (который на самом деле не делает ничего полезного) может выглядеть так:

const HOC = Component => class extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return <Component {...this.props} />;
  }
};

Он просто берет компонент, выполняет магию своего конструктора, а затем визуализирует исходный компонент, передавая свойства, которые оригинал получил бы, если бы он не был обернут (поскольку теперь компонент-оболочка получает эти свойства).

Применять этот HOC к компоненту можно двумя распространенными способами. Вы либо оборачиваете свой компонент прямо там, где вы его определяете:

const MyStatelessComponent = HOC(() => (
  <div>
   ... // More stuff
  </div>
));

или, если хотите, можете вместо этого просто обернуть оператор экспорта компонентов:

export default HOC(MyStatelessComponent);

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

Представляем: Mr. StateProvider!

Поскольку в конечном итоге мы хотим предоставить нашему компоненту состояние, давайте переименуем его в StateProvider и заставим его принимать другой аргумент - начальное состояние:

const StateProvider = (Component, initialState) => class extends React.Component {
  constructor(props) {
    super(props);
    this.state = initialState;
  }
  render() {
    return <Component {...this.props} />;
  }
};

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

const StateProvider = (Component, initialState) => class extends React.Component {
  constructor(props) {
    super(props);
    this.state = initialState;
  }
  render() {
    return <Component {...this.props} state={this.state} setState={this.setState.bind(this)} />;
  }
};

Мы просто передаем состояние и саму обычную старую функцию setState в качестве свойств нашему компоненту без состояния. Обратите внимание, как мы должны привязать this.setState (…) к «this» (компонент-оболочку), иначе это не сработает. Вы также можете передать нашему компоненту функцию стрелки, если хотите:

setState={(newState, callback) => this.setState(newState, callback)}

В любом случае теперь у нас есть доступ к состоянию в нашем компоненте без сохранения состояния, ура!

Мы на полпути - не стесняйтесь наградить себя печеньем или прохладительным напитком по вашему выбору.

Круг жизни

Чтобы легко получить доступ к методам жизненного цикла нашего компонента-оболочки, мы можем использовать тот факт, что они на самом деле являются просто еще одним свойством объекта.

Это означает, что вместо того, чтобы писать

...
didComponentUpdate() {
  // Do stuff
}
...

вы можете определить функцию где-нибудь еще, например

function doStuffWhenComponentUpdate() {
  // Do stuff
}

а затем вручную установите

this.didComponentUpdate = doStuffWhenComponentUpdate;

в конструкторе - результат будет таким же.

Итак, давайте добавим третий параметр в наш StateProvider под названием lifeCycleHooks, который является объектом, содержащим наши функции:

const componentDidUpdate = () => {
  // Do stuff
};
const componentDidMount = () => {
  // Do other stuff
};
const lifeCycleHooks = {
  componentDidUpdate,
  componentDidMount,
};
...
export default StateProvider(MyStatelessComponent, someState, lifeCycleHooks);

Затем мы можем просто перебрать переданные ключи свойств объекта, чтобы установить наши методы жизненного цикла:

Object.keys(lifeCycleHooks).forEach(
  (functionName) => {
    this[functionName] = lifeCycleHooks[functionName];
  },
);

После добавления этих улучшений наш StateProvider теперь выглядит так:

const StateProvider = (Component, initialState, lifeCycleHooks) => class extends React.Component {
  constructor(props) {
    super(props);
    this.state = initialState;
    Object.keys(lifeCycleHooks).forEach(
      (functionName) => {
        this[functionName] = lifeCycleHooks[functionName];
      },
    );
  }
  render() {
    return <Component {...this.props} state={this.state} setState={this.setState.bind(this)} />;
  }
};

Внимательный читатель мог заметить, что, поскольку мы устанавливаем функции внутри конструктора, мы не можем делать то же самое для конструктора, что и сама функция жизненного цикла. Это ситуация с курицей или яйцом.

Простое решение этой дилеммы - назвать функцию, которую мы поместили бы в наш объект lifecycleHooks, не «конструктор», а «_constructor» (чтобы мы случайно не перезаписали ее в нашем цикле Object.keys), а затем просто вручную вызвать ее внутри нормальный конструктор:

if (lifeCycleHooks._constructor) {
  lifeCycleHooks._constructor(props);
}

Эти дополнения превращают StateProvider в это:

const StateProvider = (Component, initialState, lifeCycleHooks) => class extends React.Component {
  constructor(props) {
    super(props);
    this.state = initialState;
    Object.keys(lifeCycleHooks).forEach(
      (functionName) => {
        this[functionName] = lifeCycleHooks[functionName];
      },
    );
    if (lifeCycleHooks._constructor) {
      lifeCycleHooks._constructor(props);
    }
  }
  render() {
    return <Component {...this.props} state={this.state} setState={this.setState.bind(this)} />;
  }
};

Теперь у нас есть средства доступа к методам состояния и жизненного цикла в наших компонентах без состояния, насколько это круто? Мы классная команда, ты и я, ура!

Шокирующая правда

Честно говоря, я упустил небольшую деталь, которая может испортить вам настроение -

У нас 2 компонента!

Да, я знаю - шокирует правда? Но в этом есть смысл: 1 + 1 = 2 в большинстве случаев - и это как раз подходящий момент.

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

Кроме того, наличие двух компонентов означает, что StateProvider будет отображаться во всех трассировках ошибок (например, на вашей консоли разработчика) и в дереве иерархии инструментов разработки React, что не идеально для моей книги.

Хммм… Компонент или функция?

Хорошо, что мы можем что-то с этим поделать, но не все об этом знают. Компоненты без сохранения состояния могут использоваться не только как компонент, но также могут быть напрямую вызваны как функция:

Представьте, что у вас есть компонент без сохранения состояния под названием FancyButton.

Обычно вы использовали бы это внутри другого компонента:

const OtherComponent = () => (
  <div>
   <FancyButton />
  <div>
);

В результате вы получите два компонента: OtherComponent и сам FancyButton - ничего особенного.

Но что бы произошло, если бы вы использовали FancyButton как обычную функцию?

const OtherComponent = () => (
  <div>
   {FancyButton()}
  <div>
);

Результат будет визуально идентичным, но внутри мы получим только один (!) Компонент. Это потому, что, используя его как функцию, FancyButton просто выводит свой JSX в JSX OtherComponent. Поэтому на странице он выглядит так же, но на самом деле React не превращает FancyButton в компонент. Аккуратный!

Итак, давайте сделаем то же самое в нашем StateProvider, чтобы избавиться от рендеринга и любых других накладных расходов:

const StateProvider = (Component, initialState, lifeCycleHooks) => class extends React.Component {
  constructor(props) {
    super(props);
    this.state = initialState;
    Object.keys(lifeCycleHooks).forEach(
      (functionName) => {
        this[functionName] = lifeCycleHooks[functionName];
      },
    );
    if (lifeCycleHooks._constructor) {
      lifeCycleHooks._constructor(props);
    }
  }
  render() {
    return Component({...this.props, state: this.state, setState: this.setState.bind(this)};
  });
};

По сути, мы больше не оборачиваем компонент другим компонентом, а фактически объединяем их в то, что можно было бы описать как «версию с сохранением состояния» нашего компонента без состояния.

Nomen est Omen

Теперь, когда мы избавились от дополнительного компонента, мы можем сесть и расслабиться, верно?

К сожалению нет. Каким бы замечательным ни было то, что у нас теперь есть один компонент, это также означает, что когда вы столкнетесь с ошибкой, он сообщит вам только, что ошибка произошла «в StateProvider», поскольку сам компонент без сохранения состояния теперь отсутствует. Если вы используете StateProvider в большем количестве мест, это может сильно запутать.

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

так что нам делать? Прервать? Но мы уже зашли так далеко!

Не бойтесь, решение этой дилеммы на самом деле довольно простое: Мы переименовываем наш StateProvider!

Компоненты имеют статическую функцию получения, которая возвращает их соответствующее имя.

Самое классное, что мы можем просто перезаписать его. Добавляя

const StateProvider (Component, ....) => class extends....
  static get name() {
    return Component.name;
  }
};

StateProvider вернет имя компонента без состояния вместо StateProvider. Наши проблемы решены!

Это последняя версия нашего StateProvider:

const StateProvider = (Component, initialState, lifeCycleHooks) => class extends React.Component {
  static get name() {
    return Component.name;
  }
  constructor(props) {
    super(props);
    this.state = initialState;
    Object.keys(lifeCycleHooks).forEach(
      (functionName) => {
        this[functionName] = lifeCycleHooks[functionName];
      },
    );
    if (lifeCycleHooks._constructor) {
      lifeCycleHooks._constructor(props);
    }
  }
  render() {
    return Component({...this.props, state: this.state, setState: this.setState.bind(this)});
  }
};

Заключительные мысли

Поздравляю вас с тем, что вы добрались до конца (в значительной степени) без повреждений. Немного подправив кое-что, мы фактически получили что-то, что дает нам все, что мы хотели, без каких-либо недостатков.

Кто-то может возразить, что можно добавить гораздо больше функциональных возможностей (например, предоставить компоненту без состояния доступ к «this»), но по очевидным причинам я не могу охватить их все здесь, поэтому не стесняйтесь исследовать их самостоятельно.

Если спасение зомби заставило вас всех трепетать в вашем сердце, пожалуйста, нажмите кнопку хлопка ниже и не забудьте опубликовать сообщение в Твиттере / поделиться сообщением со своими друзьями, чтобы они также были готовы к следующему зомби-апокалипсису.

Кроме того, исследования показали, что подписка на меня на Medium увеличит вашу карму как минимум на 0,0000001% (и у вас никогда не может быть достаточно кармы), а также вы получите уведомление, когда будут опубликованы новые сообщения, так что подумайте об этом, toodeloo!

Изображение предоставлено (без определенного порядка): «Счастливый зомби»: юмор, «Привет, меня зовут»: Pinterest, «оба»: парки и зоны отдыха округа Херд, «Шокирующая правда»: Youtube, «Круг жизни» : Disney / Kotaku, «Рукопожатие»: derstandard.at, «Навыки мышления высшего порядка»: Pinterest, «Hanzo»: Blizzard, «Zombie»: Pixabay