Написано Kristofer Selbekk, в сотрудничестве с Caroline Odden. Основано на лекции с таким же названием и с теми же людьми, состоявшейся на встрече ReactJS в Осло в июне 2019 года.
От переводчика — оригинальное название The 10 Component Commandments не упоминает React, но большинство примеров и рекомендаций относятся именно к реакту, к тому же статья выложена под react тэгом и написана реакт разработчиками.
Не легко создавать компоненты которыми будут пользоваться многие разработчики. Вы должны очень тщательно продумать какие пропсы использовать, если эти пропсы будут частью общедоступного API.
В этой статье мы сделаем краткое введение в некоторые лучшие практики по разработке API в целом, а также сформируем десять заповедей, применяя которые вы сможете создавать компоненты, которыми ваши коллеги-разработчики будут рады пользоваться.
Что такое API?
API — или программный интерфейс приложения — место где встречаются два куска кода. Это контактная поверхность между вашим кодом и остальным миром. Мы называем эту поверхность интерфейсом. Это определенный набор действий или точек данных с которыми можно взаимодействовать.
Интерфейс между классом и кодом, вызывающим этот класс, то же является API. Вы можете вызывать методы класса, что бы получать данные или запускать функции, заключенные в нем.
Следуя тому же принципу, пропсы, которые принимает ваш компонент, это и есть его API. Это способ при помощи которого разработчики взаимодействуют с вашим компонентом.
Некоторые лучшие практики проектирования API
Итак, какие правила и соображения применяются при разработке API? Что ж, мы провели небольшое исследование, и оказалось что есть много отличных ресурсов на эту тему. Мы выбрали два — Josh Tauberer — "What makes a good API?" и статья Ron Kurir с тем же названием — и пришли к этим четырем практикам.
Стабильные версии
Одна из самых важных вещей, которую следует учитывать при создании API — это поддерживать его как можно более стабильным. Количество критических изменений должно быть минимальным. Если вам приходиться делать критические изменения, обязательно напишите подробные руководства по обновлению и, если возможно, напишите код-мод, который автоматизирует этот процесс для разработчиков.
Описательные сообщения об ошибках
Всегда когда возникает ошибка при вызове вашего API, нужно сделать все возможное чтобы объяснить что пошло не так и как это исправить. Если вы будите ругать пользователя сообщениями вроде "неправильное использование" и не давать каких либо пояснений, ваш API оставит плохое впечатление.
Вместо этого пишите описательные ошибки, которые помогут пользователю исправить то как они используют ваш API.
Минимизировать сюрпризы для разработчиков
Разработчики хрупкие существа, и вам не стоит их пугать когда они используют ваш API. Другими словами — сделайте ваш API как можно более интуитивным. Вы достигните этого если будите следовать лучшим практикам и существующим соглашениям об именах.
Так же, ваш код всегда должен быть последовательным. Если вы используете логические имена свойств с is
или has
в одном месте, но пропускаете их дальше — это будет сбивать людей с толку.
Минимизировать поверхность API
Ваш API то же нужно минимизировать. Множество возможностей это замечательно, но чем меньше поверхность вашего API (API surface), тем меньше разработчикам придется изучать его для того что бы начать продуктивно с ним работать. Благодаря этому ваш API будет восприниматься как простой в использовании!
Всегда есть способ проконтролировать размер ваших API. Один из них — рефакторинг нового API из старого.
Десять заповедей для веб компонентов
Итак, эти четыре золотых правила хорошо работают для REST API и для старых процедурных штук на Pascal — но как перенести их в современный мир React-а?
Как мы и говорили ранее, у компонентов есть свой API. Мы называем их props
, и именно с их помощью данные передаются в компоненты. Как нам структурировать пропсы таким образом, чтобы не нарушать ни одно из приведенных выше правил?
Мы создали этот список из десяти золотых правил, которым лучше следовать при создании ваших компонентов. Мы надеемся что они будут полезны для вас.
1. Документируйте использование компонентов
Если то как нужно использовать ваш компонент не задокументировано, то этот компонент бесполезен. Ну, почти бесполезен, всегда можно посмотреть на его реализацию, но мало кто любит это делать.
Есть много способов задокументировать ваши компоненты, но мы рекомендуем обратить внимание на эти три:
Первые два дают вам площадку для работы при разработке ваших компонентов, а третье позволяет писать документацию в свободной форме при помощи MDX
Независимо от того что вы выберите, всегда документируйте как сам API, так и то как и когда ваши компоненты должны использоваться. Последняя часть критически важна в библиотеках общего назначения — что бы люди правильно использовали кнопку или сетку макета (layout grid) в заданном контексте.
2. Разрешите контекстную семантику
HTML — это язык для структурирования информации семантическим способом. Только вот большинство наших компонентов состоит из <div />
тегов. В этом есть смысл — универсальные компоненты не могут знать заранее чем они будут, может <article />
, или <section />
, или <aside />
— но такая ситуация далека от идеала.
Существует иной вариант, просто позвольте вашим компонентам принимать проп as
, и тем самым определять какой элемент DOM будет рендерится. Вот пример как можно это реализовать:
function Grid({ as: Element, ...props }) {
return <Element className="grid" {...props} />
}
Grid.defaultProps = {
as: 'div',
};
Мы переименовываем проп as
в переменную Element
и используем ее в нашем JSX. Мы даем общее значение div
по умолчанию, если у нас нет более семантического HTML-тега для передачи.
Когда придет время использовать компонент <Grid />
, вы можете просто передать правильный тег:
function App() {
return (
<Grid as="main">
<MoreContent />
</Grid>
);
}
Это работает и с React компонентами. К примеру, если вы хотите что бы компонент <Button />
рендерил React Router <Link />
:
<Button as={Link} to="/profile">
Go to Profile
</Button>
3. Избегайте логических (boolean) пропсов
Логические пропсы — это же, вроде бы, хорошая идея. Их можно использовать без значения, так что выглядит это очень элегантно:
<Button large>BUY NOW!</Button>
Но хоть это и выглядят симпатично, логические свойства допускают только две возможности. Вкл или выкл. Видимый или скрытый. 1 или 0.
Всякий раз, когда вы начинаете вводить логические свойства для таких вещей как размер, варианты, цвета или что-либо, что может быть чем-то иным, кроме двоичного выбора, у вас возникают проблемы.
<Button large small primary disabled secondary>
ЧТО Я ТАКОЕ??
</Button>
Другими словами, логические свойства часто не масштабируются с изменением требований. Вместо них для значений, которые могут стать чем-то иным, кроме двоичного выбора, лучше используйте перечисляемые значения, такие как строки.
<Button variant="primary" size="large">
Я большая важная кнопка
</Button>
Это не значит что вообще нельзя использовать логические свойства. Можно! Проп disabled
(выключенный), который я перечислял выше, все еще должен быть логическим — потому что нет никакого среднего состояния между включенным и выключенным. Просто оставьте логические свойства только для действительно бинарного выбора.
4. Используйте props.children
У React есть несколько специальных свойств, которые рассматриваются не так, как другие. Один из таких key
, нужный для отслеживания порядка элементов списка. И еще один такой специальный проп это children
.
Все, что вы помещаете между открывающим и закрывающим тегом компонента, помещается внутри пропа props.children
. И вы должны использовать это как можно чаще.
Почему? Потому что это куда проще чем иметь проп content
для контента или что-то подобное, что обычно принимает только простые значения, такие как текст.
<TableCell content="Some text" />
// против
<TableCell>Some text</TableCell>
Есть несколько преимуществ использования props.children
. Во-первых, это похоже на то как работает обычный HTML. Во-вторых, вы можете свободно передавать все что захотите! Вместо того чтобы добавлять пропсы вроде leftIcon
и rightIcon
в ваш компонент — просто передайте их как часть пропа props.children
:
<TableCell>
<ImportantIcon /> Some text
</TableCell>
Вы можете возразить что вашему компоненту нужно рендерить только обычный текст, и в некоторых случаях это так и есть. До какого-то момента. Используя же props.children
, вы гарантируете что ваш API будет готов к меняющимся требованиям.
5. Позвольте родителю цепляться за внутреннюю логику
Иногда мы создаем компоненты с большим количеством внутренней логики и состояний — например, автозаполнение или интерактивные диаграммы.
Такие компоненты часто страдают от чрезмерных API, одной из причин этого является большое количество различных вариантов использования которые накапливаются с развитием проекта.
А что если бы мы могли просто предоставить единый, стандартизированный проп, который позволить разработчику контролировать, реагировать на, или просто менять дефолтное поведение компонента?
Кент Доддс написал отличную статью о концепции state reducers. Вот статья о самой концепции, а также, вот статья о том как реализовать это для React хуков.
Вкратце, это паттерн передачи функции "state reducer" вашему компоненту, который позволит разработчику получить доступ ко всем действиям, выполняемым внутри вашего компонента. Вы можете изменить состояние (state) или даже вызывать побочные эффекты. Это отличный способ обеспечить высокий уровень кастомизации, безо всяких пропсов.
Вот как это может выглядеть:
function MyCustomDropdown(props) {
const stateReducer = (state, action) => {
if (action.type === Dropdown.actions.CLOSE) {
buttonRef.current.focus();
}
};
return (
<>
<Dropdown stateReducer={stateReducer} {...props} />
<Button ref={buttonRef}>Open</Button>
</>
}
Кстати, вы можете создать более простые способы реагирования на события (events). Использование пропа onClose
в предыдущем примере, вероятно, сделает использование компонента удобнее. Используйте "state reducer" паттерн когда это нужно.
6. Используйте оператор троеточие (spread) для оставшихся пропсов
Каждый раз когда вы создаете новый компонент — убедитесь, что вы применили троеточие к оставшимся пропсам и отправили их в элемент для которого это имеет смысл.
Вам не нужно продолжать добавлять пропсы в ваш компонент, которые просто будут переданы базовому компоненту или элементу. Это сделает ваш API более стабильным, устраняя необходимость во множестве мелких ошибок версий, когда следующему разработчику требуется новый слушатель событий (event listener) или ARIA тег.
Вы можете сделать это так:
function ToolTip({ isVisible, ...rest }) {
return isVisible ? <span role="tooltip" {...rest} /> : null;
}
Всякий раз, когда ваш компонент передает проп в вашу реализацию, например, имя класса или обработчик onClick
, убедитесь, что другой разработчик сможет сделать то же самое. В случае с классом вы можете просто добавить класс prop с помощью удобной npm библиотеки classnames (или просто конкатенацией строк):
import classNames from 'classnames';
function ToolTip(props) {
return (
<span
{...props}
className={classNames('tooltip', props.tooltip)}
/>
}
В случае с обработчиками кликов (click handlers) и с другими колбеками вы можете объединить их в одну функцию с помощью небольшой утилиты. Вот один из способов сделать это:
function combine(...functions) {
return (...args) =>
functions
.filter(func => typeof func === 'function')
.forEach(func => func(...args));
}
Тут мы создаем функцию, которая принимает список функций для их объединения. Он возвращает новый колбек, который вызывает их всех по очереди с одинаковыми аргументами.
Эту функцию можно использовать таким образом:
function ToolTip(props) {
const [isVisible, setVisible] = React.useState(false);
return (
<span
{...props}
className={classNames('tooltip', props.className)}
onMouseIn={combine(() => setVisible(true), props.onMouseIn)}
onMouseOut={combine(() => setVisible(false), props.onMouseOut)}
/>
);
}
7. Используйте значения по умолчанию
Убедитесь что вы дали достаточное количество значений по умолчанию (defaults — дефолтных значений) вашим пропсам. Таким образом вы снизите количество обязательных для передачи пропсов. Это очень упростит ваш API.
Возьмите, к примеру, обработчик onClick
. Если в вашем коде нет необходимости в этом обработчике, используйте пустую функцию (noop-function) в качестве пропа по умолчанию. Таким образом вы сможете вызывать его в своем коде, как если бы он всегда передавался.
Другой пример может быть для пользовательского ввода. Предположим, что входная строка является пустой строкой, если обратное явно не указано. Это позволит вам убедиться, что вы всегда имеете дело со строковым объектом, а не с чем-то неопределенным или нулевым.
8. Не надо переименовывать атрибуты HTML
HTML как язык имеет свои пропсы — или атрибуты, и он сам по себе является API интерфейсом элементов HTML. Так почему бы не продолжать использовать этот API?
Как мы упоминали ранее, сведение к минимуму поверхности API (API surface) и его интуитивность — это полезные методы для улучшения API ваших компонентов. Так что вместо того чтобы создавать свой screenReaderLabel
проп, почему бы просто не использовать уже существующий aria-label
?
Держитесь подальше от переименования любых существующих атрибутов HTML для вашей собственной "простоты использования". Вы ведь даже не заменяете существующий API — вы просто добавляете свой собственный поверх него. Люди по-прежнему могут передавать aria-label
вместе с вашим пропом screenReaderLabel
— и какое тогда должно быть окончательное значение?
Кроме того, убедитесь что вы никогда не переопределяете HTML атрибуты в ваших компонентах. Отличным примером является атрибут type
элемента <button />
. Это может быть submit
(по умолчанию), button
или reset
. Тем не менее, многие разработчики переопределяют этот проп для обозначения визуального типа кнопки (primary
, cta
и т.д.).
Если вы будите использовать такой проп, вы будите должны добавить переопределение для настоящего атрибута type
. Это приведет к путанице, сомнению и раздражению со стороны разработчиков.
Поверьте мне — я снова и снова совершал эту ошибку — если вы сделаете ее, потом вам придется еще долго ее расхлебывать.
9. Пишите типы пропсов (или просто типы)
Ни одна документации не будет так же хороша, как документация живущая внутри вашего кода. React поставляется с отличным способом объявлять ваши API-компоненты с помощью пакета prop-types
. Используйте его.
Вы можете указать любые требования к формату ваших обязательных и опциональных пропсов, к тому же вы можете улучшить их с помощью JSDoc комментариев.
Если вы не укажите обязательный пропс или передадите недопустимое или неожиданное значение, во время выполнения вы получите предупреждение в консоли. Это очень помогает во время разработки, и при этом может быть убрано из продакшена.
Если вы пишете свои React приложения на TypeScript или при помощи Flow, вы получаете документирование API в виде языковой функции. Это еще больше увеличивает поддержку со стороны инструментов разработки и упрощает работу.
Если вы сами не используете типизированный JavaScript, вам все равно следует подумать о том чтобы предоставить определения типов (type definitions) тем разработчикам, которые его используют. Тогда им будет куда проще использовать ваши компоненты.
10. Проектируйте для разработчиков
Наконец, самое важное правило, которому нужно следовать. Убедитесь что ваш API и работа с вашими компонентами оптимизированы для разработчиков, которые будут его использовать.
Один из способов упростить работу разработчика это давать ему обратную связь о недопустимом использовании. Делайте это при помощи сообщений об ошибках (errors), а также, но только во время самой разработки, при помощи предупреждений (warnings) о том что есть более эффективные способы использования вашего компонента.
При написании ошибок и предупреждений, давайте ссылки на свою документацию или показывайте простые примеры кода. Чем быстрее разработчик поймет в чем проблема и как ее исправить, тем удобнее для работы будет ощущаться ваш компонент.
Невероятно, но как оказалось, наличие всех этих длинных предупреждений об ошибках никак не влияет на размер финального пакета. Благодаря чудесам устранения мертвого кода весь этот текст и код ошибок могут быть убраны при сборке в продакшн.
Одна из библиотек, которая дает обратную связь невероятно хорошо, это сам React. Неважно, забыли ли вы указать ключ для элементов списка или неправильно написали метод жизненного цикла, или, может, забыли расширить базовый класс или вызвали хук неопределенным образом — в любом случае вы получите большие толстые сообщения об ошибках в консоли. Почему разработчики, которые будут использовать ваши компоненты, должны ожидать от вас чего-то меньшего?
Так что проектируйте для ваших будущих пользователей. Проектируйте для самого себя из будущего. Проектируйте для несчастных, которым придется поддерживать ваш код, когда вы ушли! Проектируйте для разработчиков.
Итого
Мы можем многому научиться у классического подхода к API. Следуя советам, хитростям, правилам и заповедям этой статьи, вы сможете создавать компоненты, которые будут легки в использовании, просты в обслуживании, интуитивно понятны и в случае необходимости, очень гибки.