Пример использования безголовых API в дизайн-системах

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

Что такое безголовый API?

Шаблон проектирования безголового API отделяет логику компонента от его визуального представления. Это шаблон пользовательского интерфейса, который (как это ни парадоксально) на самом деле не имеет пользовательского интерфейса. Вместо этого безголовый API инкапсулирует динамическую функциональность в настраиваемые хуки, которые предоставляют значения и обратные вызовы. Оттуда инженер может передать эти значения и обратные вызовы статическим визуальным элементам.

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

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

const useClickCountButton = () => {
 const [count, setCount] = React.useState(0);
 const incrementCount = setCount(count + 1);
 return { children: count, onClick: () => incrementCount };
};

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

const CounterButton = () => {
 const countButton = useClickCountButton();
 return <Ink.Button {...countButton} />;
};

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

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

ink — не первая библиотека, использующая возможности безголовых API. Downshift — отличный проект, который предлагает функции автозаполнения/выпадающего меню/выбора без какого-либо пользовательского интерфейса. Используя безголовый API, который не привязан к конкретному пользовательскому интерфейсу, инженеры могут быть уверены, что Downshift не будет конфликтовать с их уникальными требованиями к дизайну.

Создание безголового API useLedger

Компонент Ink Ledger представляет собой интерактивную таблицу, которая обрабатывает запросы API, поиск, сортировку, фильтрацию и разбиение на страницы. Он идеально подходит для отображения данных об акционерном капитале, инвестициях в фонды, членстве в совете директоров и многом другом. На сегодняшний день в экосистеме Carta существует 164 экземпляра Ledgers.

К сожалению, существует множество проблем с поддержкой Ledger. Управление визуальным рендерингом компонента с помощью реквизитов сопряжено с высокими затратами на обслуживание, что может привести к ненужным осложнениям и потенциальным ошибкам. Поскольку Ledger является таким популярным компонентом, за годы нам пришлось добавить множество реквизитов, которые управляют незначительными визуальными деталями рендеринга компонента, что привело к чрезмерно сложному API. Добавление этих свойств создало хрупкую базовую структуру кода, что затрудняет обновление как визуальной, так и поведенческой логики. И, возможно, хуже всего то, что API — это черный ящик для инженеров, использующих чернила, которые не имеют представления о том, как свойства влияют на способ рендеринга компонента.

Чтобы решить эти проблемы, мы решили создать наш первый безголовый API в качестве альтернативы компоненту Ledger. API useLedger — это специальный хук, который принимает URL-адрес и возвращает набор геттеров и сеттеров свойств, которые инженер может распространить на другие компоненты чернил, чтобы создать функционирующий реестр — без компонента Ledger. С помощью useLedger инженеры могут точно контролировать то, как их Ledger отображается на странице, без необходимости полагаться на реквизиты рендеринга.

Давайте рассмотрим пример, чтобы понять преимущества useLedger. Это пример реестра:

Чтобы построить эту книгу с помощью компонента Ledger, вы должны использовать следующий код:

<Ink.Ledger
     id="ledger-example"
     columns={[
       { label: 'Name', key: 'name', header: { sortable: true } },
       { label: 'Security', key: 'security', header: { sortable: true } },
       { label: 'Email', key: 'email' },
       { label: 'Status', key: 'status' },
     ]}
     ribbon={{
       key: 'status',
       definitions: [
         { value: 'approved', text: 'Approved', color: 'green' },
         { value: 'waiting', text: 'Waiting', color: 'orange' },
       ],
     }}
   />

Как видите, здесь задействовано несколько реквизитов, которые контролируют способ визуализации визуальных элементов в DOM. Свойство columns управляет именами столбцов и определяет, можно ли их сортировать. Свойство ленты определяет, какие строки отображаются с помощью лент в верхней левой части первой ячейки таблицы в каждой строке.

Напротив, вот как вы могли бы закодировать тот же пользовательский интерфейс с безголовым API useLedger:

const { 
  data, 
  getSearchProps, 
  selected, 
  selectableProps, 
  getSortingProps 
} = Ink.useLedger({
   url: API_URL,
 });
 
return (
   <>
     <Ink.NewInput {...getSearchProps()} />
     <Ink.NewTable id=”useledger-example”>
       <Ink.NewTable.Head>
         <Ink.NewTable.Row>
           <Ink.NewTable.HeadCell>
             <Ink.NewCheckbox id="select-all" checked={selected.allRows} onChange={selectableProps.toggleAllRows} />
           </Ink.NewTable.HeadCell>
           <Ink.NewTable.HeadCell>
             <Ink.NewTable.Pin {...getSortingProps().sortByKey('name')}>Name</Ink.NewTable.Pin>
           </Ink.NewTable.HeadCell>
           <Ink.NewTable.HeadCell>
             <Ink.NewTable.Pin {...getSortingProps().sortByKey('security')}>Security</Ink.NewTable.Pin>
           </Ink.NewTable.HeadCell>
           <Ink.NewTable.HeadCell>
             Email
           </Ink.NewTable.HeadCell>
           <Ink.NewTable.HeadCell>
             Status
           </Ink.NewTable.HeadCell>
         </Ink.NewTable.Row>
       </Ink.NewTable.Head>
       <Ink.NewTable.Body>
         {data.map(d => (
           <Ink.NewTable.Row key={d.email} selected={selected.rows[d.email]}>
             <Ink.NewTable.Cell preset="checkbox">
               <Ink.NewCheckbox id={d.email} onChange={selectableProps.toggleRow} checked={selected.rows[d.email]} />
             </Ink.NewTable.Cell>
             <Ink.NewTable.Cell>
              <Ink.NewTable.Ribbon color={getRibbonColor(d.status)} text={d.status} />
               {d.name}
             </Ink.NewTable.Cell>
             <Ink.NewTable.Cell>{d.security}</Ink.NewTable.Cell>
             <Ink.NewTable.Cell>{d.email}</Ink.NewTable.Cell>
             <Ink.NewTable.Cell>{d.status}</Ink.NewTable.Cell>
           </Ink.NewTable.Row>
         ))}
       </Ink.NewTable.Body>
     </Ink.NewTable>
   </>
 );

Хотя в подходе безголового API задействовано больше кода, в нем нет магических реквизитов, контролирующих способ отображения реестра на странице. Вместо передачи реквизита columns, который за кулисами преобразует массив в столбцы, useLedger позволяет инженеру явно управлять своими столбцами и их поведением. Вместо того, чтобы передавать sortable: true в объект конфигурации, инженер может передать реквизиты сортировки в соответствующие заголовки столбцов. Аналогичным образом, вместо управления лентами с помощью ленты, инженер может добавить в ячейку компонент ленты.

Так зачем вам создавать безголовый API, если вы можете просто использовать реквизиты рендеринга? В то время как свойства рендеринга работают в определенных обстоятельствах, они скорее являются способом привить узлы в определенный слот, чем способом позволить инженеру контролировать композицию. Безголовые API — это гораздо менее строгий способ указать, какие компоненты отображаются в DOM.

Headless API: будущее дизайн-систем

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

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

Мы продолжим добавлять безголовые API к ink и давать нашим разработчикам альтернативы его сложным монолитным компонентам, помогая нашим коллегам удовлетворять индивидуальные потребности наших клиентов, сохраняя при этом визуальную идентичность Carta. Если вы хотите помочь нам создать следующую версию Carta, мы нанимаем!