Игра Яндекс Практикума
Игра Яндекс Практикума
Игра Яндекс Практикума

Три библиотеки для React по цене одной — конкурс пет-проектов

Написал три библиотеки для React, используя JavaScript: функции вместо JSX, управление приложением с использованием хуков и CSS в React.

1К открытий5К показов
Три библиотеки для React по цене одной — конкурс пет-проектов

Мне кажется, что я начинаю свои проекты только из-за редмишек (read me). Ну знаете, как в начале придумать проблему, а потом её героически победить. Это как TDD, но только здесь не тесты, а редми вперед, прямо как функциональщики программируют, задом наперед.

Короче говоря, README — это своеобразный план, примерно как бэкендеры и фронтендеры согласовывают интерфейс до начала разработки. Здесь то же самое, но только договор с самим собой. План нужен для уверенности, что всё продумано и схвачено. Не могу не процитировать Лапенко:

Я на сто процентов уверен, что этот план должен сработать. Я вот когда его придумал, потом так в зеркало посмотрел на себя и подумал какой же я молодеец, какие же плааны я придумываю…
Антон Лапенкоинженер НИИ

А мой план таков: в рамках этой статьи я хочу поделиться с вами своим личным опытом освоения этого подхода.

Пет-проекты отличаются от обычных тем, что их делают с любовью. Пет-проекты — это не про количество, а скорее про качество. Именно поэтому я решил представить вам сразу три своих проекта. Согласен, не бьется с логикой, но это просто попытка продать три продукта по цене одной.

Прежде всего, хочу представиться. Меня зовут Султан, хотя это и звучит словно из какой-то сказки, но это моё настоящее имя. Вашему вниманию предлагаю библиотеки, написанные мной на JavaScript для React. Если вам это интересно, прошу под кат.

Stylin

Это, наверное, самый быстрый и элегантный способ создания стилизованных компонентов React. С помощью этой библиотеки можно импортировать компоненты непосредственно из файлов CSS:

			import {Title} from './styles.scss'
// crazy part, importing ↑ component from styles


<Title color="tomato" size="small">
  Hello world!
</Title>
		

На входе получаем компонент, который полностью стилизован и типизирован, а его внешний вид (стили CSS) управляется через свойства компонента.

И тут самое место задать вопрос: “Карл, как?! Как эта магия работает?”.

Одно дело — это написать в редми, другое дело — это реализовать. Благо, у меня есть фантазия и большой опыт разработки. Мне удалось найти решение в виде CSSDoc, это как JSDoc, но только для CSS. Так вот такие аннотации необходимо добавить перед стилями компонента:

			/**
  @tag: h1
  @component: Title
  size: small | medium | large
  color: #38383d --color
*/
.title {
  --color: #38383d;
  color: var(--color);
  font-size: 18px;
  
  &.small {
    font-size: 14px;
    margin: 2px 0;
  }
  &.medium {
    font-size: 18px;
    margin: 4px 0;
  }
  &.large {
    font-size: 20px;
    margin: 6px 0;
  }
}
		

А чтобы всё это заработало, мне пришлось написать специальные загрузчики для Webpack:

			npm install @stylin/style
npm install @stylin/msa-loader @stylin/ts-loade --save-dev
		

Загрузчики преобразуют аннотации в компоненты React и генерируют типы TypeScript. В зависимости от значения свойства компонента, соответствующий CSS класс будет присвоен значению className. Ниже приведенный код – это упрощенный результат конвертации для общего понимания.

			export const Title = ({
  className = '',
  color = '#38383d',
  size = '',
  children, ...rest
}) => {
  const css = `title ${size} ${className}`
  const cssVariables = {'--color': color}
  
  return (
    <h1 className={css} style={cssVariables} {...rest}>
      {children}
    </h1>
  )
}
		

Все эти преобразования и извлечение CSS стилей происходят во время сборки проекта. Это означает, что стили не создаются динамически во время выполнения приложения, как это делается в библиотеках css-in-js. Отсюда и скорость: отрисовка компонентов происходит быстро.

В целом, как всё это работает, можно посмотреть в этой демке. Более подробную информацию об этой библиотеке можно получить в README или в этой статье. Однако статья написана в пафосной форме, я был молод и не совсем понимал, что писал, в общем я предупредил.

Holycow

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

Итак, представляю вашему вниманию библиотеку holy state для управления состоянием React-приложения с использованием хуков. Эту библиотеку можно рассматривать как утилитку по созданию хуков, которые могут хранить состояние вне компонентов React. Самая интересная часть заключается в том, что она работает без использования провайдеров контекста, observables, селекторов или HOC-коннекторов.

			import {createState} from '@holycow/state'

// your store is a hook!
const useUser = createState({
  id: 1,
  name: 'Homer Simpson',
  address: {
    house: 742,
    street: 'Evergreen Terrace',
  },
})

const UserName = () => {
  const {name} = useUser() // value from the state
  return <div>{name}</div>
}

const {id, name, address} = useUser 
// any values  from the hook can be used outside of components

// subscription to whole state or specific value
const unsubscribe = useUser.subscribe('address.street', street => {
  console.log(`User street was changed to ${street}`)
})

unsubscribe() // canceling subscription
		

Основные особенности

  • Библиотека не имеет внешних зависимостей. Размер Gzip: ~1.6kb.
  • Хуки могут быть использованы за пределами компонентов React.
  • Жадная отрисовка. Компоненты перерисовываются только при обновлении значений.
  • Вычисляемые значения с кэшированием и вложенностью хуков.
  • Асинхронные экшены.
  • Подписка на изменения состояния.
  • Поддержка событийно-ориентированной архитектуры.
  • Гармония с функциональным программированием.
  • Полная типизация TypeScript.

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

			const Street = () => {
  const {address} = useUser() 
  return <div>{address.street}</div> // Evergreen Terrace
}

// no render even the address object was updated, +1 performance
useUser.set('address.house', 10)
// no render, equal value applied, +1 performance
useUser.set('address.street', 'Evergreen Terrace')
// now it will be rendered
useUser.set('address.street', 'Spooner')
		

А для более сложных задач по обновлению состояния, доступны экшены – место для размещения бизнес-логики, такой как валидация, сетевые операции и прочего:

			import {createState, action} from '@holycow/state'

const useAuth = createState({
  token: '',
  error: '',
  loading: false, // ↓ state      payload ↓
  login: action(({set, loading}) => formData => {
    if (loading) return
    set('error', '') // the state can be updated directly from the action
    set('loading', true)
    fetch('/api/login', {method: 'POST', body: formData})
      .then(res => res.json())
      .then(set('token')) // equals → .then(data => set('token', data))
      .catch(set('error'))
      .finally(() => set('loading', false))
  }),
})
		

Вычисляемые значения с поддержкой кэширования:

			import↑ {createState, computed} from '@holycow/state'

const useUser = createState({
  name: 'Peter',
	lastname: 'Griffin',
  birthDate: {
    day: 8,
    month: 12,
    year: 1979,
  },
  // lazy evaluation, function will be called when the value is used
  fullname: computed(({name, lastname}) =>
    name + ' ' + lastname
  ),
	// side effect function ↓ hook wrapper
  age: computed((state, sideEffect) =>
    sideEffect(useCurrentYear) - state.birthDate.year
  ),
})

// usage
const UserAge = () => {
  const {fullname, age} = useUser()
  // here ↑ value is calculated and cached
  return <div>{fullname} is {age} years old guy.</div>
}
		

Поддержка событийно-ориентированной архитектуры позволяет разделять код и загружать его с помощью ленивой загрузки. Такой Redux way подход, но в более простой форме.

			import {createSignal, on} from '@holycow/state'

const logout = createSignal() // creates logout signal function

// auth.js
on(logout, payload => { // listens to the logout signal
  useAuth.logout()
  console.log(payload) // → Chao!
})

// user.js
on(logout, () => {
  useUser.reset() // built-in function that restore initial state of the hook
  localStorage.removeItem('user')
})

// ↓ emits the logout signal
logout('Chao!')
		

Следующая библиотека предназначена для очень специфичных кейсов. Если вам не нравится JSX или по каким-то причинам вы не хотите использовать его в своих проектах, то следующая библиотека создана специально для вас.

React on lambda

Три библиотеки для React по цене одной — конкурс пет-проектов 3

Всё очень просто, вместо JSX используем функции. Чтобы что?

А просто, для фана:

			import λ from 'react-on-lambda' // or import l from 'react-on-lambda'
import {render} from 'react-dom'

const postLink = λ.a({href: `/posts/123`})

const title = λ.compose(
  λ.h1({class: `post-title`}), // or λ.h1({className: `post-title`})
  postLink
)

const post = λ.div(
  title(`How to use react on lambda?`),
  λ.p(`
    Lorem ipsum dolor sit amet,
    Ernestina Urbanski consectetur adipiscing elit.
    Ut blandit viverra diam luctus luctus...
  `),
  postLink(`Read more`)
)

render(
  post,
  document.getElementById(`app`)
)
		

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

			import {compose, div, ul, li, mapKey, mapProps} from 'react-on-lambda' 

const data = [
  {id: 123, name: `Albert`, surname: `Einstein`},
  {id: 124, name: `Daimaou `, surname: `Kosaka`},
]

const userList = compose(
  div({class: `followers`}),
  ul,
  mapKey(li),
  mapProps({key: `id`, children: `name`})
)

userList(data)

// jsx equivalent
const UserList = props => (
  <div className="followers">
    <ul>
      {props.data.map(user =>
        <li key={user.id}>
          {user.name}
        </li>
      )}
    </ul>
  </div>
)

<UserList data={data}/>
		

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

Тех, кого я еще не сбил с толку своими проектами, относительно RDD. Ну, о том, как я начинаю пет-проекты через README, то боюсь вас огорчить. Я, как и любой программист, не люблю писать редмишки и делаю это на последнем этапе (за редким исключением).

Мне один доктор все объяснил,RDD – это миф о загробной жизни. Кто туда попадает, тот не возвращается.
Остап Ибрагимович Бендерсын лейтенанта Шмидта

Берегите себя и прошу вас: не ешьте на ночь сырых помидоров!

Следите за новыми постами
Следите за новыми постами по любимым темам
1К открытий5К показов