Меня зовут Павленко Виталий, я Team Lead в команде UI-kit в Профи.

В какой-то момент мы хотели решить несколько проблем с UI-kit в наших React Native приложениях:

  1. UI-kit был слишком медленный. И здесь важно уточнить, что он был сделан с использованием styled-components.

  2. В UI-kit не было возможности темизации, потому что токены импортировались как модуль в каждом компоненте. И как следствие, в приложении невозможно было сделать темную тему, потому что при смене токенов оно бы просто не перерендерилось.

Спойлер. Restyle решает обе проблемы, а еще дает дополнительные классные плюшки, о которых мы обязательно поговорим дальше в статье.

Restyle – что это?

The Restyle library provides a type-enforced system for building UI components in React Native with TypeScript. It's a library for building UI libraries, with themability as the core focus.

Getting started | Restyle

Restyle предоставляет набор инструментов, которые позволят строить UI продуктов на основе токенов. Особенно очень хорошо такой подход залетает, когда в продукте есть Дизайн Система и этот набор токенов действительно определен.

Одно из основных преимуществ библиотеки Restyle по сравнению с той же styled-system, в том, что она не использует styled-components под капотом. В основе Restyle используются абстрактные стили StyleSheet, поэтому ожидается более высокая производительность.

Немного про токены

Что мы понимаем под токенами? Это цвета, отступы, радиусы и так далее – все строгие ограничения, касающиеся стилей компонентов.

У нас будет несколько групп токенов:

Цвета
{
  "profiBrand": "rgba(224, 25, 53, 1)",
  "error500": "rgba(224, 25, 53, 1)",
  "error300": "rgba(231, 78, 99, 1)",
  "error100": "rgba(255, 242, 244, 1)",
  "warning500": "rgba(255, 184, 0, 1)",
  "warning100": "rgba(255, 246, 221, 1)",
  "success500": "rgba(46, 181, 24, 1)",
  "success100": "rgba(234, 248, 232, 1)",
  "g100": "rgba(255, 255, 255, 1)",
  "g200": "rgba(244, 245, 248, 1)",
  "g300": "rgba(208, 211, 221, 1)",
  "g400": "rgba(164, 166, 178, 1)",
  "g500": "rgba(24, 24, 24, 1)",
}

Отступы
{
  "1": 4,
  "2": 8,
  "3": 12,
  "4": 16,
  "5": 20,
  "6": 24,
  "7": 28,
  "8": 32,
  "9": 36,
  "10": 40
}

Радиусы
{
  "xxs": 4,
  "xs": 8,
  "s": 12,
  "m": 18,
  "l": 20,
  "xl": 24,
  "xxl": 32
}

Типографика
{
  "headingXL": {
    "fontSize": 40,
    "lineHeight": 44,
    "letterSpacing": -0.4
  },
  "headingL": {
    "fontSize": 36,
    "lineHeight": 40,
    "letterSpacing": -0.36
  },
  "headingM": {
    "fontSize": 28,
    "lineHeight": 32,
    "letterSpacing": -0.28
  },
  "headingS": {
    "fontSize": 22,
    "lineHeight": 26,
    "letterSpacing": 0
  },
  "headingXS": {
    "fontSize": 17,
    "lineHeight": 22,
    "letterSpacing": 0
  },
  "bodyL": {
    "fontSize": 17,
    "lineHeight": 24,
    "letterSpacing": -0.34
  },
  "bodyM": {
    "fontSize": 15,
    "lineHeight": 22,
    "letterSpacing": -0.3
  },
  "bodyS": {
    "fontSize": 13,
    "lineHeight": 16,
    "letterSpacing": -0.26
  }
}

Брейкпойнты
{
  "s": 0,
  "m": 360
}

А дальше эти токены нужно расшарить на всё приложение, для этого библиотека Restyle предоставляет ThemeProvider:

import {ThemeProvider, createTheme} from '@shopify/restyle';
import {
  colors, 
  spacing, 
  breakpoints, 
  textVariants, 
  borderRadii
} from './tokens';
import {Box,Text} from './';

const theme = createTheme({
  colors,
  spacing,
  breakpoints,
  textVariants,
  borderRadii,
});

export const App = () => {
  <ThemeProvider theme={theme}>
    <Box
      flex={1}
      background="g200"
      margin={2}
    >
      <Text variant="bodyM" color="g500">Привет!</Text>
    </Box>
  </ThemeProvider>
}

Box & Text

Компоненты Box и Text являются основными строительными блоками для UI. Box создается на основе View и нужен для оберток, Text нужен для текстовых элементов соответственно. Они создаются один раз, и дальше их просто можно переиспользовать.

import {createBox, createText} from '@shopify/restyle';

export const Box = createBox(); 
export const Text = createText();

export const myComponent = () => {
  return (
    <Box
      flex={1}
      background="g200"
      margin={2}
    >
      <Text variant="bodyM" color="g500">Привет!</Text>
    </Box>
  )
};

Text имеет свой собственный проп variant, где он как раз принимает значение токена типографики textVariant .

Стили прям в разметке

Нет необходимости выносить стили отдельно или создавать отельные файлы. Стили можно добавлять прям в компоненте, используя Restyle компоненты Box и Text.

Да, все правильно, мы будем писать стили прям в JSX. А почему это хорошо?

  • Не нужно создавать бесконечные названия блоков (Container, Wrapper и т.д)

  • Мы сразу видим всю картину целиком, за что отвечает каждый блок

  • Стилей не так уж и много обычно

  • Мы все равно часто добавляем стили в JSX, будет все однообразно

  • Styled обертки сильно ухудшают производительность

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

Цвета

Цвета можно пробрасывать во все цветовые пропсы, используя ключи из темы (например g200, g300, g400).

Цветовые пропсы
  • backgroundColor [bg]

  • color

  • shadowColor

  • textShadowColor

  • borderColor

В квадратных скобках сокращенный вариант пропса

Посмотрим на пример, как это может использоваться:

<Box
  backgroundColor="g300"
  borderColor="g400"
>
  <Text
    variant="bodyS"
    color="g200"
  >
      Any text
  </Text>
</Box>

Отступы

Все отступы тоже можно пробрасывать прям в компоненты, используя специальные пропсы для этого. В нашем случае они добавляют отступы, кратные 4px (так было решено в нашей ДС).

Пропсы для отступов

margin [m], marginTop [mt], marginRight [mr], marginBottom [mb], marginLeft [ml], marginStart [ms], marginEnd[me], marginHorizontal [mx], marginVertical [my], padding [p], paddingTop [pt], paddingRight [pr], paddingBottom [pb], paddingLeft [pl], paddingStart [ps], paddingEnd [pe], paddingHorizontal [px], paddingVertical [py], gap [g], rowGap [rG], columnGap [cG]

В квадратных скобках сокращенный вариант пропса

// будет добавлен marginTop=12px и padding=8px
<Box
  marginTop={3}
  padding={2}
/>

BorderRadius

Радиус тоже берется из темы и задается следующим образом:

<Box
  borderColor="g400"
  borderStyle="solid"
  borderWidth={1}
  borderRadius="xs"
/>

Кастомные стили

Через свойство style можно пробросить и обычные стили. Таким образом можно добавить разные исключения, например, если нужен отступ некратный 4 или цвет не из ДС.

// добавится отступ 5px и кастомный цвет
<Box style={{ padding: 5, backgroundColor: '#ccc' }} />

Размеры девайсов

В нашей ДС определены два размера девайсов, что позволяет указывать разные стили следующим образом:

// s: от 0px до 360px
// m: больше 360px

// для s девайсов будет 4px отступ, а для m девайсов 8px
<Box marginTop={{ s: 1, m: 2 }} />

При помощи такой конструкции можно пробрасывать любые свойства, не только отступы.

Если написать просто marginTop={1}, то стили применятся для всех устройств (s и m).

Если нам нужно написать какую-то логику в компоненте, опираясь на размеры девайсов, то можно использовать хук useResponsiveProp():

import {useResponsiveProp} from '@shopify/restyle';

export const MyComponent = () => {
  const value = useResponsiveProp({
    s: 'Сейчас S устройство',
    m: 'Сейчса M устройство'
  });
  
  return <Text>{value}</Text>;
};

 Использование токенов из темы

Нам не нужно импортировать нужные токены из темы, можно нужно просто достать их используя хук useTheme().

import {useTheme} from '@shopify/restyle';

export const MyComponent = () => {
  const {colors: {g300}} = useTheme();
  return <OutsideComponent color={g300}>Привет!</OutsideComponent>
};

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

Производительность

Мы произвели исследование по производительности 3 вариантов и получили интересные результаты!

Как именно измерили

Для теста был создан следующий компонент:

  • Компонент содержал 100 строк

  • В каждой строке был номер, созданный шрифтом из ДС

  • В каждой строке было 20 блоков, с радиусом из ДС и рандомным цветом из ДС

Проводилось 10 замеров рендера для каждого варианта, затем для результата бралось среднее значение.

Измерялось время рендера следующим образом:

export const Component = () => {
  const start = new Date().getTime();
  useEffect(() => {
    const renderTime = new Date().getTime() - start;
    console.log(renderTime);
  }, []);

  return (
    ...
  );
};

Вариант UI

Время рендера (милисекунды)

StyleSheet

408

Restyle

499

Styled-components

632

Оказалось, что Restyle на порядок быстрее Styled-components, но естественно немного медленнее вообще нативного решения, потому что содержит дополнительные обертки и парсер атрибутов в стили.

Итог

Что мы получаем от Restyle:

  • Удобные “строительные блоки” Box и Text

  • Удобная работа с токенами ДС через атрибуты

  • Все токены достаются через хуки из Restyle контекста

  • Удобочитаемые композиции верстки сразу со стилями

  • Понятная и однозначная концепция для построения UI 

Эта библиотека точно заслуживает внимания. Не вижу причин не брать ее на вооружение прям сейчас! Спасибо за внимание ????

Комментарии (4)


  1. strelkan
    28.09.2023 07:10

    Restyle 499
    Styled-components 632
    Оказалось, что Restyle на порядок быстрее Styled-component

    на порядок в нашей десятичной системе значит - с добавлением еще одной цифры в число, то есть хотя бы в 10 раз, а ваша разница на 1/6, в 100мс на страницу вообще не стоила всех этих трудов


    1. vital_pavlenko Автор
      28.09.2023 07:10
      +1

      Ладно ????


  1. SergeyMild
    28.09.2023 07:10

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


    1. vital_pavlenko Автор
      28.09.2023 07:10

      Быстрее, чем styled-components, пруфы есть ???? В styled-components также создаются стили в рендере, а не при старте приложения. А про jsx это вкусовщина