Еволюція шаблонів проектування в React

9 хв. читання

Уважніше поглянемо на деякі шаблони проектування, що виникають в екосистемі React. Ці шаблони підвищують читабельність, чистоту та полегшують повторне використання компонентів.

Я почав працювати з React близько 3 років тому. У той час не було усталених практик, вивчаючи які та слідуючи яким можна було б поліпшити якість своїх розробок.

Спільноті знадобилося близько двох років щоб виробити кілька ідей. Ми перейшли від React.createClass до ES6 class та чистих функціональних компонентів, відмовились від міксинів та спростили API

Тепер, коли спільнота більша, ніж будь-коли, ми спостерігаємо еволюцію декількох цікавих шаблонів проектування.

Для того, щоб зрозуміти ці шаблони, вам необхідні базові уявлення про React та його екосистему. Проте зверніть увагу, що у цій статті вони не охоплені.

Отже, почнемо!

Умовне виконання (conditional rendering)

Я бачив наступний сценарій у багатьох проектах.

Коли люди думають про React і JSX, вони все ще думають про HTML та JavaScript.

В результаті цілком логічним кроком є відокремлення умовної логіки від коду, що повертається.

const condition = true;

const App = () => {
  const innerContent = condition ? (
    <div>
      <h2>Show me</h2>
      <p>Description</p>
    </div>
  ) : null;
  
  return (
    <div>
      <h1>This is always visible</h1>
      { innerContent }
    </div>
  );
};

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

В якості альтернативи спробуйте наступний шаблон, де ви скористаєтесь особливістю мови.

const condition = true;

const App = () => (
  <div>
    <h1>This is always visible</h1>
    {
      condition && (
        <div>
          <h2>Show me</h2>
          <p>Description</p>
        </div>
      )
    }
  </div>
);

Якщо condition є false, другий операнд оператора && не визначається. Якщо true — повертається другий операнд, тобто JSX код.

Це дозволяє, використовуючи декларативний підхід, змішувати логіку користувацького інтерфейсу з його фактичними елементами!

Слід розглядати JSX як не від'ємну частину коду! Зрештою, це просто JavaScript.

Надсилання властивостей вниз по дереву (passing down props)

Коли застосунок зростає, у вас є менші компоненти, які працюють як контейнери для інших компонентів.

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

Хорошим способом обійти це, використати деструктуризацію властивостей разом з оператором розпакування JSX, як ви можете побачити тут:

const Details = ( { name, language } ) => (
  <div>
    <p>{ name } works with { language }</p>
  </div>
);

const Layout = ( { title, ...props } ) => (
  <div>
    <h1>{ title }</h1>
    <Details { ...props } />
  </div>
);

const App = () => (
  <Layout 
    title="I'm here to stay"
    language="JavaScript"
    name="Alex"
  />
);

Отже, тепер ви можете змінити властивості необхідні для Details і бути впевненим що ці властивості не надсилаються на кілька компонентів.

Деструктуризація властивостей (destructuring props)

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

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

const Details = ( { name, language } ) => (
  <div>
    <p>{ name } works with { language }</p>
  </div>
);

class Details extends React.Component {
  render() {
    const { name, language } = this.props;
    return (
      <div>
        <p>{ name } works with { language }</p>
      </div>
    )
  }
}

Цей шаблон полегшує трансформацію компоненту. Також ви обмежуєте використання this всередині компонента.

Шаблон «Постачальник» (provider pattern)

Ми розглядали приклад де властивості надсилались через інший компонент. А якщо вам доведеться надіслати їх до 15 компонентів?

У подібній ситуації корисно буде скористатися React Context. Не сама рекомендована функція React, але вона виконує свою роботу.

Нещодавно оголосили, що в Context з'являється новий API, який реалізує шаблон постачальника з коробки.

Якщо ви використовуєте такі бібліотеки, як React Redux або Apollo, ви, можливо, знайомі з цим шаблоном.

Дізнайтесь як це працює з сьогоднішнім API, це допоможе вам зрозуміти новий API. Ви можете поекспериментувати на цьому гральному майданчику.

Компонент верхнього рівня — провайдер (provider) —  записує певні значення в контекст. Дитячі компоненти, що називаються споживачами (consumers), візьмуть ці значення з контексту.

Поточний синтаксис контексту є дещо дивним, але майбутня версія реалізує саме цей шаблон.

Компоненти вищого порядку (high order components)

Обговорімо багаторазове використання коду. Разом з відмовою від фабричної функції React.createElement(), команда React відмовилась від міксинів. Вони були, певною мірою, стандартним способом композиції компонентів через композицію простих об'єктів

Компоненти вищого порядку (HOC) —  призначенні, щоб заповнити необхідність в багаторазовому використанні поведінки компонентів.

HOC — функція, яка приймає вхідний компонент і повертає розширену/модифіковану версію цього компоненту. Те що ми називаємо HOC має багато імен, я вважаю за краще сприймати його як декоратори.

Якщо ви використовуєте Redux, ви розпізнаєте функцію connect як HOC, яка бере компонент і додає до нього купу властивостей.

Реалізуємо базовий HOC, який може додавати властивості до наявних компонентів.

const withProps = ( newProps ) => ( WrappedComponent ) => {
  const ModifiedComponent = ( ownProps ) => ( // модифікована версія компонента
    <WrappedComponent { ...ownProps } { ...newProps } /> // оригінальні props + нові props
  );

  return ModifiedComponent;
};

const Details = ( { name, title, language } ) => (
  <div>
    <h1>{ title }</h1>
    <p>{ name } works with { language }</p>
  </div>
);

const newProps = { name: "Alex" }; // this is added by the hoc
const ModifiedDetails = withProps( newProps )( Details ); // hoc is curried for readability

const App = () => (
  <ModifiedDetails 
    title="I'm here to stay"
    language="JavaScript"
  />
);

Якщо вам подобається функціональне програмування, вам сподобається працювати з компонентами вищого порядку. Recompose — чудовий пакет, що дає вам багато таких утиліт: withProps, withContext, lifecycle тощо.

Поглянемо на дуже корисний приклад багаторазового використання функціоналу.

function withAuthentication(WrappedComponent) {
  const ModifiedComponent = (props) => {
    if (!props.isAuthenticated) {
      return <Redirect to="/login" />;
    }

    return (<WrappedComponent { ...props } />);
  };

  const mapStateToProps = (state) => ({
    isAuthenticated: state.session.isAuthenticated
  });

  return connect(mapStateToProps)(ModifiedComponent);
}

Ви можете використовувати withAuthentication, коли в маршруті потрібно показати чутливий вміст. Цей вміст буде доступний тільки авторизованим користувачам.

Це приклад наскрізного функціонала реалізованого в одному місці для багаторазового використання в усьому застосунку.

Проте у компонентів вищого порядку є свої недоліки. Кожен HOC створить додатковий компонент в структурі React DOM/vDOM. Це, при збільшенні застосунку, може призвести до потенційних проблем з продуктивністю.

Рендеринг властивостей (render props)

Не зважаючи на те що HOC і Render props є взаємозамінними, Я не можу віддати абсолютну перевагу одному з них. Обидва шаблони використовуються для покращання багаторазового використання та чистоти коду.

Ідея полягає в тому, що ви передаєте контроль над вашою функцією рендеринга іншому компоненту, який потім передає вам контроль через властивість функції.

Деякі люди вважають за краще використовувати для цього динамічні властивості, деякі просто використовують this.props.children.

Це все досить бентежить, але подивимося на простий приклад.

class ScrollPosition extends React.Component {
  constructor( ) {
    super( );
    this.state = { position: 0 };
    this.updatePosition = this.updatePosition.bind(this);
  }
  
  componentDidMount( ) {
    window.addEventListener( "scroll", this.updatePosition );
  }

  updatePosition( ) {
    this.setState( { position: window.pageYOffset } )
  }

  render( ) {
    return this.props.children( this.state.position )
  }
}

const App = () => (
  <div>
    <ScrollPosition>
      { ( position ) => (
        <div>
          <h1>Hello World</h1>
          <p>You are at { position }</p>
        </div>
      ) }
    </ScrollPosition>
  </div>
);

Тут, в якості властивості, використовуємо children. Ми передаємо в компонент <ScrollPosition> функцію, яка приймає параметр position.

Рендеринг властивостей слід використовувати в ситуаціях коли вам потрібно повторювати деяку логіку в середині компонента і ви не хочете обертати компонент в HOC.

В бібліотеці React-Motion можна знайти багато прикладів використання цього шаблону.

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

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