progway — программирование, IT
2.68K subscribers
25 photos
1 video
246 links
Чат: @prog_way_chat

Разборы вопросов и задач с собеседований, мысли, полезные материалы и просто вещи, что мне интересны из мира IT

Полезности и навигация в закрепе

По всем вопросам: @denisputnov
Download Telegram
Список доступных хештегов:

Основные
:
#theory — общая теория программирования, разбор теоретических вопросов с собеседования
#quiz — короткий вопрос на свободную тему в разработке с вариантами ответов
#useful — просто полезные вещи
#blog — посты в формате блога обо мне / на свободную тему

Подгруппы:
#javascript — всё, связанное с языком
#typescript — аналогично 👆
#code — посты во встроенным в текст кодом, готовые примеры
#vite — посты, которые так или иначе затрагивают сборщик
#web — всё, касательно web разработки
#principles — принципы проектирования
#react — всё, касательно React
#patterns — всё о паттернах
#data — всё о данных и манипуляциях с ними
#news — новости

@deprecated
#python — всё, связанное с этим языком
#mobile — мобильная разработка
#design — штучки для дизайна
#github — интересности с гита
#chatbot — мои боты и всё, что с ними связано
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Именованный и неименованный экспорт

Начнем с того, что экспорт бывает разный — именованный и неименованный.

Именованный экспорт — это использование ключевого слова export перед каждой сущностью или при использовании «паттерна» export list, когда все экспортируемые сущности перечисляются в одном месте файла:

// именованный экспорт
export const a = 1

// export list
const c = 3
const d = 4
const f = 5

export {
c,
d,
f
}


Стандартный же экспорт — это экспорт с использованием конструкции export default:

// стандартный экспорт
const b = 2
export default b


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

import { 
Status,
getUser,
render as renderFunction
} from './file'


Стандартный импорт:

import React from 'react'


А также их комбинация:

import React, { useState, useMemo } from 'react'


Но что насчет проблематики? Почему разработчики каждый раз сталкиваются с вопросом какой лучше экспорт выбрать?

Чтобы понять это, рассмотрим следующий пример:

import Angular from 'react' // абсолютно валидно

import { Status as UserStatus } from './file'


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

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

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

Мой вывод: я стараюсь сократить использование export default до минимума, предпочитая именованный экспорт и импорт. Использовать export default валидно только для интеграции вашего кода с уже готовыми решениями, например, это может быть React.lazy и React.memo, которые работают только с export default по умолчанию. У меня есть удобный хак как это обойти, но это тема для отдельного поста.

Спасибо за прочтение, это важно для меня ❤️

#web #javascript #react #patterns
👍2911🐳3🔥21🤔1
CSS можно в JS и это не всегда хорошо

Фронтендеры как будто не могут договориться как им хочется писать стили 🙂. Вариантов очень много, разнообразных препроцессоров и инструментов в виде библиотек стилей, CSS фреймворков…

Вообще, CSS-in-JS — это некоторый подход, в котором мы отказываемся от массового использования стандартных CSS файлов и пишем стили внутри JS. Для реализации этого концепта есть несчётное количество библиотек, но для React основной я бы назвал styled-components. Лично я вижу повальное число проектов, в которых используется эта библиотека, да и сам использовал её в достаточно часто.

Причина проста: это очень удобно, по нескольким причинам.
1. CSS-in-JS позволяет генерировать стили динамически для одного класса. Стандартный CSS так не может, мы должны изменять стили только добавлением дополнительного класса, модификатора и т.д.
2. Дополнительная мета-информация при разработке переезжает из className в название самого тега. Если мы хотим как-то влиять на внешний вид нашего приложения, то классы мы будем записывать в переменные, соответственно прочитать их быстро не получится. В общем, сложно. Стилизованные компоненты же несут информацию о том, зачем они нужны, сразу из названия, и этой информации куда больше, чем просто div, aside, ul...
3. Упрощённая файловая структура. Формочки можно клепать ещё проще и быстрее, не переключаясь между файлами.

Возможно и звучит круто, но за всё это приходится платить достаточно высокую цену:
1. Стили создаются в рантайме и блокируют выполнение остального JavaScript на странице. Это приводит к проблемам с производительность, особенно для перегруженных систем. Всё это удобство в разработке приведет к тому, что пользователь будет плеваться от того, какое медленное приложение вы сделали.

Использование нативного CSS для браузера куда понятнее и быстрее, JS бандл становится меньше и грузится также быстрее, а стили подгружаются параллельно вашему коду.

В результате-то что? На мой вкус, CSS-in-JS — это и правда очень удобно. Но я полностью отказался от этого подхода в своих проектах, потому что не готов платить производительностью за все плюсы, что описал выше.

Если вы хотите писать стили быстро и качественно, то я действительно рекомендую обратить большее внимание на обычные модульные стили на каком-то препроцессоре или же вообще на Tailwind CSS. От последнего я, кстати, невероятно долго плевался, ну а потом как обычно: отрицание, гнев, торг, депрессия, влюбился… Точно рекомендую посмотреть, если ещё не пробовали)

Спасибо за прочтение, это важно для меня ❤️

#javascript #web #react #theory
👍3813🔥3🐳3
Страшные рефы

Есть код:

const items = [1, 2, 3, 4];
const itemRefs: React.RefObject<HTMLDivElement>[] = items.map(() =>
useRef(null),
);

// и далее в вёрстке
ref={itemRefs[index]}


Как сделал бы я:

// вынести из компонента
const items = [1, 2, 3, 4];

// внутри компонента
const itemRefs = useRef([])

// и внутри вёрстки
// index берется из .map, так как элементы на
// страницу вставляются из списка
ref={element = inputRef.current[index] = element}


Цитата из доки реакта: “Не вызывайте хуки внутри циклов, условных операторов или вложенных функций. Вместо этого всегда используйте хуки только внутри React-функций, до возврата какого-либо значения из них.”. Если это правило не соблюдать, то можно самых странных ошибок огрести.

Подробнее в официальном руководстве

@prog_way_blog#react #review
🔥11🐳92🤔1
Зачем нужен useCallback

Очень распространенная ошибка, которая говорит о том, что мало кто понимает что такое useCallback и зачем он нужен.

Важный вопрос, будет ли ререндерится Component при ререндере родителя?

const Parent = () => {
const foo = useCallback(() => {

}, [])

return <Component foo={foo} />
}

const Component = (props) => { ... }


И ответ тут очевиден — конечно же да! Не смотря на то, что функцию мы мемоизировали и ссылка на саму функцию у нас не изменилась благодаря useCallback, мы всё равно перерендерим Component, поскольку рендерится родитель.

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

const Component = memo((props) => { ... })


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

А ещё много таких было кейсов:

const Component = () => {
const foo = useCallback(() => {

}, [])

return <button onClick={() => foo()} />
}


Тут смысл useCallback теряется вдвойне, так как в пропсах мы всё равно пересоздаём функцию на каждый рендер вот тут: onClick={() => foo()}

Пример из доки реакта

@prog_way_blog#react #review
👍20🐳4🔥3🤔1
Как сохранять состояние в URL

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

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

www.site.com/?name=progway


Соответственно, нам нужен инструмент, который позволит управлять этим состоянием. Как бы хотелось это видеть? Лично мне — ровно так же, как работает и обычный useState. Тогда от этого и будем отталкиваться:

const [name, setName] = useQueryState("name");
const [surname, setSurname] = useQueryState("surname", "Putnov");


Первым аргументом будем указывать ключ в URL, а вторым — изначальное состояние. Далее рассмотрим внутреннюю имплементацию хука и начнём с его интерфейса, тут всё достаточно просто:

type QueryValue = string | undefined;

function useQueryState<I extends QueryValue>(
queryKey: string,
defaultValue?: I,
) {
// тело хука
}


Реализуем наш собственный хук на базе useSearchParams из react-router-dom для удобного доступа к состоянию URL-a. Если вы используете другой роутинг, можно использовать аналогичный механизм, который подойдёт именно в вашем проекте. Мы же будем его использовать для изменения URL-a и восстановления состояния из URL-a при маунте компонента. Если в URL-е уже есть состояние, мы будем использовать его. Если нет, будем использовать defaultValue:

const [searchParams, setSearchParams] = useSearchParams();

const [state, setState] = useState<QueryValue>(() => {
const targetValue = searchParams.get(queryKey);

if (targetValue !== null) {
return targetValue;
}

return defaultValue;
});


Ну а всё что нужно далее — реагировать на изменение состояния и обновлять URL в соответствии с ним. Если в инпуте вдруг окажется пустая строка, то мы просто удалим наше состояние из Query параметров

useLayoutEffect(() => {
setSearchParams((prev) => {
if (state) {
prev.set(queryKey, state);
} else {
prev.delete(queryKey);
}
return prev;
});
}, [state, queryKey, setSearchParams]);


Да и всё. Всё работает.

Вот весь код, который, конечно же, не идеальный. Смысл поста — отразить основную суть.

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#theory #code #typescript #web #react
🔥26👍7🤯4🐳31🍌1
Че там в React 19

У меня сейчас, к сожалению или к счастью, нет времени полностью посмотреть React Conf, которая длится аж 4 часа, что имхо очень много.
Я пролистал конфу и нашёл пару интересных моментов, которыми хочу поделиться тут.

Как я понял, React Compiler — это что-то, что стоит сравнивать скорее с компилятором TypeScript. Также один из спикеров сравнивает его с компиляторами типа WebAssembly и Hermes, который сейчас используется в React Native. По сути, всё, что он делает — разбирает весь ваш код и представляет его в виде полностью контролируемого графа. Сам по себе компилятор содержит очень много оптимизаций, типа автоматического удаления неиспользуемого кода, распространения констант и ещё куча умных слов. Все эти техники оптимизации не новы, но спикеры подмечают, что, конечно же, подсосали всё из Rust и C++, что не удивительно.

Цель такого компилятора — дополнительно оптимизировать весь наш код без видимых изменений для разработчика, тем самым улучшить и DX, и UX.

Что наиболее интересно — компилятор приносит нам такое интересное понятие как Computation Graph. Как я понимаю, это понятие мы ещё не раз услышим в будущем.

Computation Graph — граф, который отображает вычислительные зависимости между сущностями в коде. Показывает то, каким образом данные влияют на другие данные и на вёрстку в целом. Что-то типа useMemo и других хуков, когда мы напрямую указывали реакту, изменение чего конкретно нужно отслеживать и что конкретно нужно пересчитать. Теперь этим полностью занимается компилятор.

Тут обращаем внимание на скрины. Со 2 по 6 мы видим то, что такое Computation Graph и как он получается из нашего кода. На 7 скрине мы видим как это работает. Из полученного графа мы можем узнать минимально нужный перечень сущностей для обновления, то есть что конкретно мы хотим обновить, при этом не затрагивая остальные несвязанные ветви графа.

Во что конкретно компилятор собирает это — я пока не досмотрел. Но даже эта ограниченная информация — очень интересно)

@prog_way_blogчат#theory #react #news
👍245🔥2🤯1🐳1
Что такое виртуализация?

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

Я очень рекомендую TanStack Virtual для этой задачи. Мне уже удалось применить его на нескольких проектах и никаких проблем он не вызывал


Если объяснять на пальцах как это работает, то я бы сказал так:
Мы можем представить, что хотим послушать музыку. Открываем что-то типа Тындыкс.Музыки и видим 10.000 разных треков — это 10.000 строк огромного списка

Так вот, зачем отображать все 10.000 сразу, если на экране в лучшем случае поместиться треков 20-30? Давайте рисовать только первые, например, 50 треков, а дальше следить за тем что видит пользователь — за его скроллом. Если пользователь прокрутит ещё на 20 строк вниз, то мы нарисуем следующие 20 строк, а первые 20 удалим из вёрстки

Под коробкой все виртуализаторы как раз этим и занимаются:
1. Определяют какие элементы видны пользователю сейчас
2. Определяют сколько элементов скрыто над и под окном просмотра

Чаще всего это реализуют через события scroll на контейнере или через IntersectionObserver, подробности реализации для особо любопытных всегда доступны в сорсах библиотек (в TanStack Virtual можете начать с функции observeElementRect)

Виртуализация может пригодиться в огромном срезе приложений, поэтому очень советую с ней ознакомиться заранее, если вы ещё не успели

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#theory #javascript #code #react
🐳26👍95🔥4
Как менять состояние вкладки по интервалу

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

Для реализации нам необходимо создать состояния, между которыми мы хотим перемещаться:
const states = [
{ title: "Мессенджер", icon: Favicon1 },
{ title: "Новое сообщение", icon: Favicon2 },
]


Далее необходимо завести состояние, которое будет определять на каком этапе из списка состояний мы сейчас находимся. Это состояние можно будет менять через обычный интервал:
const [stateIndex, setStateIndex] = useState(0);

// раз в секунду переходим на следующее состояние
useEffect(() => {
const intervalId = setInterval(() => {
setStateIndex((prevIndex) => (prevIndex + 1) % states.length);
}, 1000);

return () => clearInterval(intervalId);
}, []);


И в зависимости от текущего состояния изменяем параметры вкладки:
useEffect(() => {
const link = document.querySelector("link[rel~='icon']");
const title = document.querySelector("head title");

if (link) {
link.href = states[stateIndex].icon;
title.textContent = states[stateIndex].title;
}
}, [stateIndex]);


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

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#code #web #theory #javascript #react
🐳22😐8👍54🔥2
Составные компоненты

Есть такой паттерн для реакта, который называется Compound Components. Это можно перевести как "составные компоненты"

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

То есть мы можем заранее объединить компоненты каким-то контекстом и переиспользовать их, например, через общее пространство имён, в качестве которого чаще всего выступает родительский компонент


Запутались в словах? Лучше посмотреть в коде:

Вот пример прям из доки Ant Design:
import { Layout } from 'antd';

<Layout>
<Layout.Header>Header</Layout.Header>
<Layout.Content>Content</Layout.Content>
<Layout.Footer>Footer</Layout.Footer>
</Layout>


Обратили внимание, как компоненты Header, Content и Footer мы получаем напрямую из компонента Layout? Это и есть пример паттерна Compound Components. Компоненты связаны, а используются они из общего пространства — компонента Layout

Зачем же так сделали? Тут преследуется три цели:
1. Явно показать на уровне нейминга, что использовать Layout.Footer вне Layout не нужно
2. Расшарить общий контекст между всеми компонентами Layout
3. Корректно стилизовать части Layout в зависимости от значения внутри общего контекста

С неймингом и стилями, думаю, всё предельно ясно. Но что насчёт контекста? На самом деле, Layout под собой содержит ещё и LayoutContext, который содержит в себе состояние компонента Sider и распространяет его на все дочерние компоненты. Схематически это выглядит примерно так:

InternalLayout  
└ LayoutContext <-- инициализируем контекст
├ Header
├ Content <-- а в этих компонентах получаем его значение
├ Footer
└ Sider


В итоге получится, что все дочерние компоненты, пытающиеся получить доступ к контексту, без обёртки работать будут криво или и вовсе не будут

С точки зрения типов всё тоже не сложно. Тот же Ant Design делает так:
import InternalLayout, { Content, Footer, Header } from './layout';
import Sider from './Sider';

// получаем тип базового layout компонента
type InternalLayoutType = typeof InternalLayout;

// создаём тип, который определит какие компоненты мы вложим в layout
type CompoundedComponent = InternalLayoutType & {
Header: typeof Header;
Footer: typeof Footer;
Content: typeof Content;
Sider: typeof Sider;
};

// нагло переприсваиваем тип
const Layout = InternalLayout as CompoundedComponent;

// нагло биндим нужные компоненты
Layout.Header = Header;
Layout.Footer = Footer;
Layout.Content = Content;
Layout.Sider = Sider;

// не менее нагло экспортируем как public-api
export default Layout;


Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#web #javascript #typescript #theory #code #react #patterns
👍268🔥5🐳3
Связываем React и localStorage через useSyncExternalStore

Как согласовать изменение состояния в реакте и поля в localStorage? До недавнего времени самым простым вариантом было создать контекст с внутренним React состоянием и обрабатывать всё взаимодействие с localStorage через него — вариант рабочий, но далеко не идеален: легко напороться на ререндеры, много кода писать нужно ну и вот это вот всё

Также можно обработать какое-то не-реактовое значение через комбинацию useState + useEffect, но это ещё менее надёжно, ведь браузерные значения могут меняться и без уведомления реакта, и, соответственно, без ререндера

Красиво в одной из статей на хабре описали:

Для работы с состоянием в React используются хуки useState и useReducer, но они не умеют работать с состоянием, которое "живет" за пределами React, поскольку в один момент времени доступна только одна версия внешнего состояния.

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

Статья: https://habr.com/ru/companies/timeweb/articles/720136/


Но не так давно в 18 версию React добавили хук useSyncExternalStore, который такую задачу решает намного изящнее

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

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

Как раз этот хук и поможет нам интегрироваться с localStorage сильно проще и безопаснее. Тут localStorage в понятие внешнего хранилища ложится просто шикарно

На коленке код будет выглядеть примерно так:

const useLocalStorageState = (key: string, defaultValue?: string) => {
const subscribe = (listener: () => void) => {
window.addEventListener("update-local-storage", listener);
return () => void window.removeEventListener("update-local-storage", listener);
};

const getSnapshot = () => localStorage.getItem(key) ?? defaultValue;

const store = useSyncExternalStore(subscribe, getSnapshot);

const updateStore = (newValue: string) => {
localStorage.setItem(key, newValue);
window.dispatchEvent(new StorageEvent("update-local-storage", { key, newValue }));
};

return [store, updateStore] as const;
};


В чём тут идея:
1. При вызове updateStore будем помимо изменения значения в localStorage диспатчить на window ещё и StorageEvent с ключом, например, "update-local-storage"
2. В функции подписки subscribe объясним когда нужно вызывать getSnapshot для получения актуального состояния из внешнего хранилища и когда от его прослушивания нужно отписаться. Можно воспринимать как эффект

Использовать будем как обычный useState:
const [name, setName] = useLocalStorageState("name", "progway");


Теперь хук при вызове с одним и тем же ключом к localStorage (name в примере выше) будет обновлять все зависимые компоненты при регистрации события "update-local-storage" на window

Используя тот же подход, можно реализовать порой очень полезные хуки useMediaQuery, useWindowSize и другие. О первых двух можно прочитать в статье от Timeweb Cloud

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#web #javascript #theory #data #code #react
Please open Telegram to view this post
VIEW IN TELEGRAM
28👍11🔥6🐳2
Флоу рендеринга компонента в React

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

➡️При маунте порядок следующий:

1. Рендер на основе изначальных значений состояний
2. useInsertionEffect
3. Создание DOM
4. Прикрепление ссылок на ноды (ref)
5. useLayoutEffect
6. useEffect

➡️При апдейте компонента:

1. Рендер на основе новых значений состояний
2. Обновление DOM
3. useInsertionEffect
4. Прикрепление ссылок на ноды (ref)
5. useLayoutEffect
6. useEffect

📎Решил вынести её в канал, потому что сам прям недавно сталкивался с этим на рабочем проекте и подумал, что это тоже кому-то может быть полезно

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

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#web #theory #react
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥34🐳511👍1
Верстка писем — это боль? или нет..?

Если вы думали, что адаптивная верстка для всех браузеров — это сложно, попробуйте сверстать email, который одинаково выглядит в Gmail, Outlook, Yahoo и десятках других почтовых клиентов...


Вёрстка письма — это крайне специфичное занятие, при котором нет возможности использовать добрую половину, если не больше, возможностей HTML и CSS

Почтовые движки настолько старые, что многие из них до сих пор требуют верстки с использованием таблиц, прямо как в 90-х. Но гораздо интереснее вопрос — почему получилось именно так? Я бы выделил пару причин:

➡️ Что и так понятно, причина историческая

Вёрстка письма — задача часто тривиальная. Вот многие и не заморачиваются с обновлением движков, так как это не дает какой-то бизнес выгоды. Нет абсолютно никакой разницы между вёрсткой письма для отправки PIN-кода для входа на таблицах или на флексах

➡️ Безопасность

Если углубиться и задуматься, то окажется, что у большинства запретов есть свой вполне объективный смысл. Есть много способов "заставить" вёрстку сходить куда-то за данными:

@font-face { src: url(...) }
background-image: url(...)
list-style-image: url(...)

<form action="https://.../steal.php" method="get">


И ещё куча других примеров — везде так или иначе наше письмо отправляет какой-то запрос. А это уже опасно тем, что открывает кучу возможностей для слежки за пользователем. Например, можно узнать реальный IP пользователя просто при открытии письма (открытие письма → запрос картинки → трекинг IP)

Также, например, запрещён position:fixed, потому что позволяет фиксировать элементы на экране, что можно использовать для фишинга. Например, можно зафиксировать модалку поверх письма с призывом ввести пароль, и она будет выглядеть как часть интерфейса почтового клиента

Но самое главное, что всё реально развивается и стало лучше!


— Outlook теперь рендерит письма на движке от Edge, а не от Microsoft Word (блин, да! до 2022 года письма реально рендерились на основе... ворда! )
— почти все почтовые клиенты научились понимать <div /> 😎
— много где появилась поддержка медиа запросов
— ну и много чего ещё

А самое главное, что появились крутые инструменты для вёрстки писем, которые из коробки закрывают много проблем — когда-то относительно давно появился MJML и сделал небольшую революцию, а в последние годы так и вовсе очень сильно хайпят react-email или vue-email

Мне недавно довелось сверстать десяток писем на react-email и с уверенностью могу сказать, что это было совсем-совсем не больно:
— тут тебе и tailwind
— и аля аналог storybook для шаблонов
— кастомные шрифты
— куча шаблонов и примеров кода
— поддержка markdown для текстов
— куча фиксов для почтовых клиентов...

И
всё это чудо в удобной обёртке из коробки с поддержкой компонентного подхода без смс и даже без регистрации

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

Ну и react-email тоже рекомендую, приятный инструмент

Спасибо за прочтение, это важно для меня ❤️

@prog_way_blogчат#useful #web #react
Please open Telegram to view this post
VIEW IN TELEGRAM
👍326🔥6🐳2🤔1