Создаем React-компоненты иконок с помощью Figma API и SVGR. Часть 2.
Tags: react
Published: 2022-11-16 11:01:18

[Исходники первой части](https://github.com/Sm1t/figma-svgr-icons-part-1)
[Исходники второй части](https://github.com/Sm1t/figma-svgr-icons-part-2)

В первой части мы автоматизировали загрузку **svg** иконок из **Figma**. Теперь нам предстоит преобразовать их в готовые к использованию React-компоненты и наделить некоторым API, например цвета и размеры. Для этого мы будем использовать **SVGR** и его расширенные возможности. Мы будем дорабатывать [скрипт из первой части](https://github.com/Sm1t/figma-svgr-icons-part-1/blob/master/scripts/icons/load-icons.js) и в итоге полностью автоматизируем процесс от нарисованной иконки в Figma до готового React-компонента.

## SVGR

Думаю SVGR не нуждается в представлении: по поисковому запросу "convert svg to react" в Google первым результатом будет SVGR. А всем известный [create-react-app](https://github.com/facebook/create-react-app) использует [@svgr/webpack](https://react-svgr.com/docs/webpack/).

SVGR предоставляет [CLI](https://react-svgr.com/docs/cli/) с опцией `--out-dir`, которая позволяет преобразовать папку целиком, а также [возможность кастомизировать шаблон](https://react-svgr.com/docs/custom-templates/), благодаря чему мы сможем наделить наши иконки некоторым API.

Помимо этого SVGR использует [SVGO](https://github.com/svg/svgo/) для оптимизации кода SVG перед преобразованием его в компонент и Prettier для форматирования JavaScript. Всё это, конечно, можно настраивать. У этого инструмента ещё много возможностей и достоинств. Узнать обо всём подробнее вы сможете [в документации](https://react-svgr.com/docs/what-is-svgr/).

Мы будем использовать SVGR версии **6.2.1**, которая является последней на момент написания статьи. С выходом шестой версии было несколько важных обновлений, ознакомиться с которыми можно в [гайде по миграции](https://react-svgr.com/docs/migrate/).

## Конвертация svg-иконок в React-компоненты

Начнём с небольшого обновления **icons.config.js**. Хотелось бы видеть итоговые React-компоненты в папке **icons**, в которую сейчас загружаются svg-исходники. Давайте это изменим и будем загружать исходники иконок в папку **icons_sources**. Для этого обновим **iconsFolder** в нашем конфиге **icons.config.js**:

```javascript
module.exports = {
  ...
  iconsFolder: 'icons_sources',
  ...
}
```

Теперь можем приступать к созданию компонентов иконок. Добавим **@svgr/cli** в наш проект.

```
yarn add --dev @svgr/cli
```

Затем создадим конфигурационный файл **svgr.config.js** и укажем **outDir**, куда мы хотим сохранять компоненты:

```javascript
module.exports = {
  outDir: 'icons',
}
```

После чего мы уже можем запустить команду:

```
yarn svgr icons_sources
```

И сразу получим набор React-компонентов:

![Пример компонента](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5qsur6hyaofb3mvhvuz.png) 

Более того, SVGR автоматически сгенерировал файл **index.js** с реэкспортами всех компонентов. Как мы видим, SVGR по умолчанию приводит названия компонентов к формату **PascalCase**, например для иконки **add_alert.svg** компонент **AddAlert.js** соответственно.

![Экспорты иконок](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ci41v4qbwgzdc5xrctc1.png) 

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

**Во-первых**, необходимо заменить цвет заливки по умолчанию на **currentColor**, чтобы в дальнейшем можно было управлять цветом иконки через css-свойство **color** для элемента **svg**. SVGR предоставляет возможность модифицировать атрибуты перед преобразованием с помощью опции [replaceAttrValues](https://react-svgr.com/docs/options/#replace-attribute-value). Давайте воспользуемся этой возможностью,:

```javascript
module.exports = {
  outDir: 'icons',
  replaceAttrValues: {
    '#323232': 'currentColor',
  },
}
```

> цвет наших иконок мы можем подсмотреть [в макетах](https://www.figma.com/file/x2vqkyeGdrL0nnSbLslveL/Material-Design-Icons-(Community)?node-id=0%3A1) или в одной из загруженных svg-иконок — в нашем случае это **#323232**

**Во-вторых**, нам нужно, чтобы атрибут **viewBox** остался после преобразования. Сейчас он удаляется, так как в [дефолтном SVGO-пресете](https://github.com/svg/svgo#default-preset) включен плагин **removeViewBox**: viewBox удаляется в том случае, если он соответствует значениям атрибутов ширины и высоты. Иконки Material как раз попадают под это правило.

Настройки SVGO мы можем указать с помощью опции [svgoConfig](https://react-svgr.com/docs/options/#svgo-config). Согласно [документации SVGO](https://github.com/svg/svgo#default-preset), мы можем настраивать плагины с помощью параметра **overrides**. Нам необходимо отключить плагин removeViewBox, поэтому настройки будут выглядеть следующим образом:

```javascript
module.exports = {
  outDir: 'icons',
  replaceAttrValues: {
    '#323232': 'currentColor',
  },
  svgoConfig: {
    plugins: [
      {
        name: 'preset-default',
        params: {
          overrides: {
            removeViewBox: false,
          },
        },
      },
    ],
  },
}
```

**В-третьих**, мы можем удалить необязательный атрибут **xmlns**. Для этого в плагины SVGO добавим **removeXMLNS**:

```javascript
svgoConfig: {
  plugins: [
    ...
    'removeXMLNS',
  ],
},
```

Это все настройки, которые нам необходимы на текущий момент. Давайте выполним команду `yarn svgr icons_sources` и убедимся, что в компонентах используется currentColor, viewBox и отсутствует xmlns:

![Пример компонента после настроек SVGR](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g7rw8rlsw1lswfwghsyb.png)  

Чтобы в дальнейшем выполнять загрузку иконок из figma и их преобразование в React-компоненты за одну команду, можно добавить в package.json соответствующий скрипт:

```json
// package.json
...
"scripts": {
  ...
  "icons": "yarn load-icons && svgr icons_sources"
```

## Storybook

Чтобы проверить компоненты в действии, предлагаю подключить [Storybook](https://storybook.js.org/docs/react/get-started/install). Им также будет удобно пользоваться при разработке API для иконок. Не будем отвлекаться на описание этого инструмента: думаю, многие с ним знакомы. А если нет, рекомендую срочно [это исправить](https://storybook.js.org/).

Storybook при установке смотрит в зависимости и определяет конфигурацию, поэтому давайте добавим в зависимости **react** и **react-dom**. Так как наш пример — это "библиотека" иконок, то react и react-dom устанавливаем в **peerDependencies**, а чтобы работал Storybook, дублируем в **devDependencies**:

```
yarn add --peer react react-dom
// and
yarn add --dev react react-dom
```

После чего вызываем команду для добавления Storybook:

```
npx sb init
```

После того как эта команда отработает, в проект добавится папка **stories** с примерами по умолчанию.

Удалим всё содержимое папки **stories** и добавим два новых файла. Первый — **Icons.js**, в котором импортируем все иконки и рендерим их в компоненте **Icons**:
```javascript
// stories/Icons.js
import React from 'react';

import * as icons from '../icons';

export const Icons = () => (
  <>
    {Object.values(icons).map((Icon, index) => (
      <Icon key={index} />
    ))}
  </>
);
```

Второй, **icons.storis.mdx**, — страница с отображением иконок:
```markdown
// stories/Icons.stories.mdx
import { Meta } from '@storybook/addon-docs';
import { Icons } from './Icons';

<Meta title="Icons" />

## Иконки

<Icons />
```

После чего можно запустить Storybook:

```
yarn storybook
```

Теперь мы видим, что компоненты иконок отлично работают:

![Иконки в Storybook](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2pme9e1xjukxcv2abtth.png)

## API иконок

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

![Передача пропсов в компоненте](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qohrrxpfbgmpn9yflswg.png)

Мы можем в этом убедиться, добавив атрибуты **color**, **height** и **width** при создании элементов в **stories/Icons.js**:

```javascript
// stories/Icons.js
...
<Icon key={index} color="orange" height={48} width={48} />
```

После чего мы увидим соответствующие изменения в Storybook:

![Иконки в Storybook с цветом и размером](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/efy3h5e80qp47pc4f0o7.png) 

Но просто возможность указать любой цвет и размер не всегда то, что нужно. Чаще всего необходимо предоставить некоторый стандартизированный набор цветов и размеров. Для этого нам нужно получить контроль над конечным набором параметров для **svg**.

### SVGR Custom Template

Как мы видим из примеров выше, по умолчанию у компонентов корневой элемент — **svg**, для которого указаны параметры по умолчанию  (из svg-исходников иконки), а через `{...props}` прокидываются все остальные параметры. Но если заменить корневой элемент на некоторый компонент **SvgIcon**, то в этом компоненте мы получим доступ ко всем входящим параметрам и сможем управлять финальным набором параметров для **svg**.

> Данный [подход с SvgIcon](https://mui.com/api/svg-icon/) используется в Material

SVGR предоставляет возможность задать шаблон, который будет использоваться при преобразовании иконок в компоненты — [Custom Templates](https://react-svgr.com/docs/custom-templates/). С помощью этой возможности мы заменим корневой элемент компонентов на компонент **SvgIcon**, который мы реализуем чуть позже. Для этого добавим наш custom template в **svgr.config.js**:

```javascript
const { types } = require('@babel/core');

module.exports = {
  ...
  template: function svgrCustomTemplate(
    { imports, interfaces, componentName, props, jsx, exports },
    { tpl }
  ) {
    // меняем корневой элемент на SvgIcon
    jsx.openingElement.name.name = 'SvgIcon';
    jsx.closingElement.name.name = 'SvgIcon';

    // https://github.com/gregberge/svgr/issues/530
    // при изменении корневого элемента пропадает спред пропсов
    // поэтому необходимо добавить спред пропсов самостоятельно
    jsx.openingElement.attributes.push(
      types.jSXSpreadAttribute(types.identifier('props'))
    );

    return tpl`
  ${imports};
  import { SvgIcon } from '../SvgIcon';
  
  ${interfaces};
  
  const ${componentName} = (${props}) => (
    ${jsx}
  );
   
  ${exports};
  `
  }
}
```

После выполнения команды `yarn svgr icons_sources` мы увидим, что наши иконки приняли новый облик:

![Скриншот с иконкой Add.js](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gjvzliruymnjxteuuigq.png)

Теперь можно приступать к реализации компонента SvgIcon.

### SvgIcon

Прежде всего давайте определимся с цветами и размерами, которые мы будем предоставлять. Для примера возьмем три цвета из [палитры цветов Material](https://mui.com/customization/palette): **Error**, **Warning** и **Info**:

![Цвета Material](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6q8t1xxx2j3isd14dpjw.png)

А также два размера из [Meterial Icons](https://mui.com/components/icons/#size): **small** и **large**, значения которых **20x20** и **35x35** соответственно.

В итоге мы получим следующие наборы цветов и размеров:

```javascript
const colors = {
  error: '#ef5350',
  info: '#03a9f4',
  warning: '#ff9800',
}

const sizes = {
  'small': 20,
  'large': 35,
}
```

Как мы уже обсуждали выше, по умолчанию в корневой элемент компонента передаются следующие параметры:
- **width**, **height** и **viewBox** — размеры и viewBox по умолчанию
- **children** — содержимое svg-элемента: path, clipPath и др.
- **{...props}** — все остальные параметры

Чтобы наделить наши иконки API цветов и размеров, нам необходимо на стороне компонента SvgIcon реализовать поддержку параметров **color** и **size**, с помощью которых и будет происходить управление размером и цветом.

Следовательно, в компоненте SvgIcon мы делаем следующее:
- получаем с помощью деструктуризации параметры **width**, **height**, **children**, **color** и **size** 
- все остальные параметры собираем в `...props`
- возвращаем svg-элемент, в который первым делом пробрасываем все параметры `...props`, а значения **color**, **height** и **width** определяем на основе параметров **color** и **size**
- пробрасываем **children** в svg-элемент.

Для простоты примера будем указывать стили с помощью атрибутов.

```javascript
export const SvgIcon = ({
  children, color, height, size, width, ...props
}) => {
  return (
    <svg
      {...props}
      color={colors[color] || color}
      height={sizes[size] || height}
      width={sizes[size] || width}
    >
      {children}
    </svg>
  );
};
```

Компонент SvgIcon в итоге выглядит следующим образом:

```javascript
// SvgIcon/SvgIcon.js
import React, { forwardRef } from 'react';
import { node, oneOf } from 'prop-types';

const colors = {
  error: '#ef5350',
  info: '#03a9f4',
  warning: '#ff9800',
}

const sizes = {
  'small': 20,
  'large': 35,
}

export const SvgIcon = forwardRef(function SvgIcon(
  { children, color, height, size, width, ...props },
  ref
) {
  return (
    <svg
      {...props}
      color={colors[color] || color}
      height={sizes[size] || height}
      width={sizes[size] || width}
      ref={ref}
    >
      {children}
    </svg>
  );
});

```

Для того чтобы посмотреть, как работает SvgIcon компонент, давайте добавим для него историю в storybook.

```javascript
// stories/SvgIcon.stories.js
import React from 'react';

import { SvgIcon } from '../SvgIcon';

export default {
  title: 'SvgIcon',
  component: SvgIcon,
};

const Template = (args) => (
  <SvgIcon width={24} height={24} viewBox="0 0 24 24" {...args}>
    <path d="M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z" fill="currentColor"/>
  </SvgIcon>
);

export const Playground = Template.bind({});
```

Storybook автоматически создает [элементы управления](https://storybook.js.org/docs/react/essentials/controls) для аргументов на основе PropTypes или типов TypeScript. Давайте добавим PropTypes для нашего компонента.

```
yarn add prop-types
```

```javascript
// SvgIcon/SvgIcon.js
import { node, oneOf } from 'prop-types';
...

SvgIcon.propTypes = {
  children: node,
  color: oneOf(Object.keys(colors)),
  size: oneOf(Object.keys(sizes)),
};
```

Теперь мы сможем посмотреть на работу компонента SvgIcon в storybook:

![SvgIcon storybook](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rny2qskdvk918qves178.gif)

И таким API будет наделён каждый компонент-иконка. Теперь мы можем использовать иконки следующим образом:

```javascript
import { AddAlert, Warning, ErrorOutline } from '../icons';

const Example = () => {
  return (
    <>
      <AddAlert size='large' color='info' />
      <Warning color='warning' />
      <ErrorOutline size='small' color='red' />
    </>
  )
}
```

![Пример использования иконок](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mm8pnrymcxcf72r5tq1b.png)

## Заключение

Таким образом, объединив скрипт загрузки иконки из первой части и преобразование svg-иконок в React-компоненты из второй, мы можем полностью автоматизировать процесс от иконки в Figma до готового к использованию компонента, наделённого некоторым стандартизированным API.

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