Стандарт розробки React для початківців

Усім привіт, мене звати Максим Маслов, і це моя перша стаття на DOU. Вона розрахована на усіх React-розробників рівня trainee/junior і стосується тих робочих випадків, коли немає сильної потреби у використанні state-менеджменту.

Структура проєкту

Для забезпечення організованості та структурованості кодової бази важливо дотримуватись загальної файлової структури та ієрархії застосунку.

Ознайомтесь з наступним скріном.

Приклади:

Папка assets може містити папки images, fonts, styles

Папка blocks може містити папки:

  • MyProfile, яка буде містити в собі папки з блоками AboutMe, MyContacts і т.д.
  • Settings, яка буде містити в собі папки з блоками ContactSetting, PaymentsSettings і т.д.
  • common, яка буде містити в собі папки з загальними блоками Header, Footer, Sidebar і т.д., які використовуються в багатьох частинах застосунку.

і тому подібні блоки.

Блоки треба обовʼязково розділяти за папками, щоб було зрозуміло, який блок до якої сторінки належить.

Папка components може містити форми, а також компоненти, які складаються з ui (Input + Button), які надалі можуть бути теж перевикористані в якості єдиного компоненту.

І так далі. Треба обовʼязково зберігати ієрархію папок, щоб завжди було інтуїтивно зрозуміло, що до чого належить. Навіть якщо в папці blocks/MyProfile/AboutMe буде тільки 1 файл (AboutMe.jsx), все одно він повинен бути у своїй папці AboutMe.

Саме собою зрозуміло, що якщо в проєкті додатково зʼявляються якісь глобальні зміни, наприклад застосування state-менеджменту, то зʼявляється відповідна папка, наприклад src/redux.

Принцип єдиної відповідальності

Компоненти мають бути максимально простими та спеціалізованими. Замість створення одного великого компонента, розділіть його на менші частини зі своїми власними обов’язками та функціональністю. Це полегшить розуміння і тестування коду.

Наприклад, якщо у вас є складний компонент User, ви можете розділити його на UserInformation та UserActions, де перший відповідає за відображення інформації про користувача, а другий — за взаємодію з користувачем.

Приклад:

Кожний компонент має бути окремим файлом, а якщо в нього є власні стилі або окремі додаткові файли, які до нього відносяться, то для цього повинна бути створена окрема папка всередині основної папки компонента User.

Використовуйте кастомну обгортку для однакових блоків

Якщо у вас є однакові блоки або компоненти, варто розглянути можливість створення кастомної обгортки. Це дозволить вам забезпечити єдиний код, полегшить його утримання і пришвидшить подальшу розробку.

Приклад:

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

Не страшно, якщо у вашому проєкті використовується декілько різних обгорток. Для кожної варто створити свій компонент:

І надалі це можна використовувати таким чином:

До речі, помітили яка крива верстка у LinkedIn ?

Використовуйте map() для відображення однакових компонентів

Приклад, як не варто робити:

Чому цей підхід — погана звичка? В багатьох випадках ми не можемо знати завчасно скільки у нас буде елементів і який буде їхній зміст, тому що дуже часто такі дані приходять з бекенду.

І якщо в пет-проєкті, коли ви робите якесь меню, то так ще спрацює, а ось в реальному житті вам все одно доведеться використати map().

Приклад, як правильно:

Створюємо в окремому файлі масив, який повинен містити в собі дані елементів. Якщо це заглушка для тестування і надалі нам ці дані будуть приходити з бекенду, то можна створити в папці mock, а якщо це незмінні днні, то можна в папці definitions або в папці data.

Далі, відображаємо кожний елемент:

Також обʼєкти з інформацією про елементи можуть містити і функції, наприклад, які будуть спрацьовувати при кліці на елемент.

Зверніть увагу, метод map() першим параметром приймає елемент, а в цьому випадку ми його деструктурували і замість listItems.map((item) => ...) написали listItems.map(({id, text}) => ...), таким чином ми дістали зміст цього item.

Використовуйте кастомний хук замість імпорту контексту

Якщо вам потрібно передати дані або функції кільком компонентам, краще використовувати кастомний хук замість імпорту контексту. Це дозволяє зробити код зрозумілішим та пришвидшить розробку.

Зазвичай, для доступу до контексту, в провайдер якого обгорнута ділянка коду, до якої він належить, ми звертаємось наступним чином:

Замість цього можна імпортувати хук useContext з «react» безпосередньо в файл нашого контексту і в кінці цього файлу створити простий кастомний хук, який буде виглядати таким чином:

Завдяки цьому, в умовах налаштованого автоімпорту, ми дуже швидко можемо звертатись до нашого контексту таким чином:

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

Використовуйте локальний контекст для окремих багатофункціональних компонентів

Якщо у вас є компонент, який виконує багато функцій або має велику кількість обов’язків, розгляньте можливість використання локального контексту, навіть якщо ваш застосунок вже огорнутий глобальним контекстом. Глобальний контекст може бути корисним для передачі загальних даних або функцій на рівень застосунку, але використання його для кожного компонента призводить до зайвого ререндерингу.

Оскільки при оновленні будь-якого стану в глобальному контексті відбувається ререндеринг усіх компонентів, які використовують дані з цього контексту, варто розглянути винесення деяких станів в окремий локальний контекст. Це допоможе уникнути зайвого ререндерингу тих компонентів, які не залежать від цього стану.

У деяких випадках можна використовувати мемоїзацію локальних контекстів, щоб запобігти зайвому ререндерингу. Це передбачає кешування значень контексту і його використання лише при зміні необхідних даних. Однак, це більш просунута тема, яка потребує глибшого вивчення та розуміння. Ми до неї повернемося пізніше.

Важливо також зазначити, що при використанні глобального контексту необхідно зберігати якомога менше глобальних станів. Чим більше станів зберігається в глобальному контексті, тим більше компонентів будуть ререндеритись при їх зміні. Тому розсудливим підходом є обмеження глобальних станів до необхідного мінімуму.

Перевикористання функцій і кастомні хуки

Якщо якась функція може бути задіяна в двох або більше компонентах, варто її винести окремим файлом у відповідну папку: utils/helpers/function.js.

Якщо ця функція містить хуки, то вона називається кастомним (користувацьким) хуком. Її назва повинна починатись зі слова «use» і міститись вона повинна у відповідній паці: utils/hooks/useCustomHook.js

Багато кому не зрозуміло поняття кастомного хука. Ось приклад:

Своєю чергою цей хук може використовуватись в компонентах наступним чином:

Використовуйте defaultProps*

defaultProps — це стандартна властивість, яка використовується в React для встановлення значень за замовчуванням для пропсів, які очікує компонент, якщо вони не передані або мають значення undefined. Використовується ця властивість тільки в тому випадку і тільки для тих пропсів, без яких компонент не зможе правильно функціювати.

Загалом, використання defaultProps допомагає зробити компонент гнучкішим, надійнішим і зрозумілішим, спрощуючи розробку та підтримку коду.

Приклад:

В такому випадку, якщо у нас не прийде image в пропсах, то замість стандартної браузерної «зломаної іконки» буде відображено дефолтне зображення. Такий приклад ви можете побачити в кожній соціальній мережі. Це маленький приклад, і теоретично його можна замінити наступним кодом:

І хоча другий варіант, здається, має меншу кількість рядків, пріоритетним завданням є досягнути максимальної читабельності коду. Тому, безумовно, варіант з використанням defaultProps є набагато зрозумілішим. Крім того, в реальному житті бувають ситуації, коли компоненту потрібно визначити багато значень за замовчуванням, і використання другого підходу може призвести до спотворення коду.

*В деяких ситуаціях, наприклад, якщо ми використовуємо forwardRef, defaultProps може не спрацьовувати. В такому випадку можна задавати значення за замовчуванням при деструктуризації пропсів самому атрибуту:

const Avatar = ({ image = defaultImage }) => {}

Є думка, що такий підхід більш очевидний, але я раджу його використовувати тільки якщо неможливо використати defaultProps.

Використовуйте бібліотеку PropTypes

Якщо в проєкті ми не використовуємо TypeScript, то без додаткових зусиль і знань, ми можемо частково закрити питання валідації типів, а конкретно валідацію пропсів.

PropTypes дозволяє вказати очікуваний тип для кожної властивості компонента, такий як рядок, число, масив, об’єкт і т.д. Це допомагає уникнути передачі неправильних типів даних і непередбачуваної поведінки. А також дозволяє вказати, які властивості є обов’язковими для передачі до компонента.

Це забезпечує правильне використання компонента і уникнення помилок виклику. Вже на етапі розробки, завдяки помилці в консолі, ми будемо знати, що до нас в пропсах в конкретний компонент прийшов, наприклад, масив, але цей компонент очікує на обʼєкт.

Приклад:

В цьому випадку ми зазначили обовʼязкові типи даних, які ми очікуємо, а також те, що нам обовʼязково повинен прийти проп name.

Якщо ми передамо в пропси невірні типи даних, то нас про це одразу попередить бібліотека.

Ще один приклад:

І хоч прописувати в компоненті інлайн-стилі — це не дуже гарна практика, але суть повинна бути зрозуміла. Якщо у нас в дизайні можливі тільки три конкретних кольори для кнопки, то ми прописуємо їх перелік у відповідному масиві, а також зазначаємо, що колір обовʼязково повинен бути переданий.

Неочікуваний колір в цьому випадку викликає помилку.

Ще один приклад:

В цьому випадку ми прописали, що допустимі значення text — це або рядок або число. І можна подумати, ну хто буде передавати, наприклад, обʼєкт в такому випадку? Я навів просто максимально зрозумілий приклад. В реальних проєктах з сотнями компонентів, і коли пропси можуть буди результатом якихось обчислень, бібліотека PropTypes може заощадити розробнику купу часу.

Інтуїтивно зрозумілі назви змінних і жодних скорочень

Тут все зрозуміло із заголовка. Наприклад, назва UserEdBtn є поганим прикладом, а UserEditButton є хорошим прикладом. Не заощаджуйте на довжині назв компонентів. Вони повинні ясно та чітко відображати їхню функціональність, щоб ми не мусили шукати по всьому проєкту, що саме робить ця змінна і звідки вона береться.

Наприклад, FooterSocialLinksTitle є нормальною назвою, але не треба перебільшувати. FooterMenuSocialLinksTitleText — вже занадто довга назва.

Так само раджу чинити в циклах і методах:

Замість цього:

Писати так*:

*Використовувати index в якості key в реальних проєктах — погана практика, особливо якщо кількість елементів може динамічно змінюватись. Таке можна робити тільки у випадках, коли ми знаємо, що наш масив жодним чином не буде змінений.

В інших випадках для key треба шукати альтернативні варіанти значень. Якщо ми знаємо, що listItem.text у нас ніколи не повторюється, то краще використати його. А якщо ми самі створюємо масив, за яким відмальовуємо елементи, то в обʼєкт з елементом можна додати id і його вже використовувати в якості key.

Це все базовий мінімальний набір правил, щоб зробити проєкт максимально підтримуваним. Існує ще дуже багато інструментів і практик, окрім вищеперерахованих, але початківцю забивати ними голову поки не варто.

Якщо у когось є якісь коментарі, питання, зауваження чи пропозиції, пишіть у коментарях або ж мені в телеграм.

Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!

👍ПодобаєтьсяСподобалось26
До обраногоВ обраному18
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

з більшою вірогідністю проект буде рости. а тому хотілося б огляд структури проекту з TS, RTK, RTK Query, react router dom, class-transformer, MUI. де типи, де інтерфейси, де DTO. з лібами на форми. я нещодавно задумався чи зможу я замість типів використовувати класи і навішувати на них функції типу user.isAdmin(). на серверній стороні в ентіті я такі штуки додав і це робить код більш читабельними і коротким. наприклад коли береш юзерів з бази users.filter(user => !user.isAdmin()).
а цей приклад як на мене трішки відірваний від реальності. вже на чистий JS боляче дивитись.
я виконував таски на декількох проектах і всюди якась своя структура папок і стиль іменування файлів. це іноді на якомусь моменті хтось схалтурить і потім починається повна анархія і пиши як хочеш бо рефакторити і часу нема і зламати щось не хочеться. бо ще ж протестити всі моменти треба після цього.

Цікава стаття, цікавий погляд на ще один варіант структури проєкту.

Хотілося б більше бест-практіс по кастомним хукам. З мого досвіду найбільшою проблемою з ними є практика «sweep under the rug», коли складну логіку з величезного компонента виносять в хук, навіть не намагаючись відокремити generic логіку. А потім повторюють це кілька разів, огортаючи величезний хук в кілька рівнів інших величезних хуків.

Використовуйте бібліотеку PropTypes

Для React Native проєктів не раджу використовувати reactnative.dev/...​n-071#restoring-proptypes

А загалом підтримка пакету слабка github.com/facebook/prop-types/tags

Використовуйте defaultProps*

Stop using defaultProps (React 18.3 preview) www.youtube.com/watch?v=fnQusZt49_Y

Дякую за статтю. Пора вже забувати про PropTypes і переходити на TypeScript

Дізнався про ще один вид архітектури Rect папки.

Цікаво скільки їх всього?🧐

Як краще тепер налаштовувати зборку spa-проекту?

— CRA deprecated
— Next.js оверхед, плюс вендор-лок на Vercel
— Vite — вендор-лок на одного китайця, якого може збити автобус. Також він думаю явно буде віддавати перевагу вью, а реакту в останню чергу. Плюс різний процес зборки під капотом для дев і прод режиму, що потенційно може викликати важко-відтворювані баги.
— кастомний вебпак-конфіг не прийнятний, шукаємо якийсь свіжий офіційний all-in-one стартер-шаблон.

Так начебто ви наявні способи і перерахували.

Зайшов на create-react-app.dev — ніде не побачив, що проект deprecated. Все ще “Create React App is an officially supported way to create single-page React applications.”

Якщо відкрити документацію React, то ви не знайдете там згадки «create-react-app», але знайдете згадки про Next.js і Remix (react.dev/...​start-a-new-react-project). Через те, що документація React ігнорує «create-react-app», цей пакет можна вважати застарілим.

Через популярність Next.js, мені здається, що він став стандартним способом використовувати React і для мене очевидно брати саме його на новий проєкт.

Чому ви вважаєте, що Next.js є оверхедом?
Чому ви вважаєте, що існує вендор-лок на Vercel?

Чому ви вважаєте, що Next.js є оверхедом?

Мав на увазі чисто для класичного spa-проекту. Але може і так.. там же ш наче була опція експорту в статичні файли (але ж можуть і це прикрити.)). Тому зараз мабуть доречно брати тільки офіційний next.js, робити в дев-режимі одну сервер-сторінку, і в ній вже будувати свій spa. Після чого для проду вигружати на «свої (не vercel) сервери»

мм..

Навіть якщо в папці blocks/MyProfile/AboutMe буде тільки 1 файл (AboutMe.jsx), все одно він повинен бути у своїй папці AboutMe.

- якщо цей блок явно прив’язано до myProfile, то навіщо його виносити, а не зафігачити прямо в pages? І як краще — AboutMe/AboutMe.tsx чи AboutMe/index.tsx?

Використовуйте бібліотеку PropTypes

TS не буде достатньо?

FooterSocialLinksTitle є нормальною назвою

навіть якщо це локальна змінна блоку SocialLinks компоненту Footer ?

Так само раджу чинити в циклах і методах:

замінити (e,i)=>.... на (listItem, index)=> ... — чим це краще? i,j,k — класичні олдскульні назви змінних для індексу. listItem — навіщо внутрішній функції знати, що це саме list item а не просто item? якщо завтра ви будете обходити не Array а Object, то доведеться перейменовувати на objectProperty? ownObjectProperty?

Дякую за статью, я хоч і не джун, але деякі поради знайшов корисними для своїх проектів. Вважаю, що фронтенд лідам потрібно краще прописувати і уніфікувати саме структуру проекта.
Також було б цікаво почитати аналогічну статью для React + TypeScript + State manager застосунків.

Підписатись на коментарі