Найкращі практики проєктів на React із Typescript

18 хв. читання

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

Поглянемо ближче!

Як React та TypeScript працюють разом?

Перш ніж почнемо, пригадаємо, як React та TypeScript працюють разом. React — «JavaScript-бібліотека для створення користувацьких інтерфейсів», водночас TypeScript — це «типізована надбудова над JavaScript, яка компілюється в нього». Використовуючи обидві технології в тандемі, ми створюємо UI за допомогою типізованого JavaScript.

Варто застосовувати згадані технології разом, аби отримати переваги статично типізованої мови (TypeScript) при створенні інтерфейсу.

TypeScript компілює мій React-код?

Досить часто це питання цікавить розробників. Насправді весь процес відбувається подібно до діалогу:

TS: «Привіт! Це твій UI-код?»

React: «Ага».

TS: «Круто! Я скомпілюю його та переконаюсь, чи не забув ти про щось».

React: «Звучить класно!»

Тобто відповідь: так, однозначно! Але пізніше, коли ми торкнемося tsconfig.json, більшість часу вам захочеться використовувати "noEmit": true. Це означає, що TypeScript не буде генерувати JavaScript після компіляції. Все тому, що ми просто беремо TypeScript для перевірки типів.

Результат обробляється в налаштуваннях create-react-app за допомогою react-scripts. Ми запускаємо yarn build — і скрипти React збирають наш проєкт для продакшена.

Отже, TypeScript компілює React-код, аби провести перевірку типів. Він не генерує вихідний JavaScript (у більшості випадків). Результат і далі має вигляд звичайного React-проєкту.

Чи може TypeScript працювати з React і Webpack?

Так, TypeScript працює з React і Webpack. На щастя для вас, в офіційному посібнику з TypeScript є інформація про налаштування.

Нарешті, освіживши трохи знання про взаємодію React і TypeScript, перейдемо до найкращих практик.

Найкращі практики

Автори матеріалу дослідили найпоширеніші запитання та створили перелік найбільш корисних та популярних варіантів використання React і TypeScript. З ними ваші проєкти будуть ще досконалішими.

Конфігурація

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

  • tsconfig.json;
  • ESLint;
  • Prettier;
  • VS Code extensions and settings.

Перейдемо до налаштувнаня проєкту.

Найшвидший спосіб почати роботу з React/TypeScript — використати create-react-app з TypeScript-шаблоном. Для цього виконайте команду:

npx create-react-app my-app --template typescript

Так ви отримаєте мінімальну конфігурацію для роботи з React і TypeScript. Декілька помітних відмінностей:

  • наявність розширення .tsx;
  • наявність файлу tsconfig.json;
  • наявність файлу react-app-env.d.ts.

Розширення tsx означає TypeScript JSX. tsconfig.json — це конфігураційний файл TypeScript зі стандартними налаштуваннями. react-app-env.d.ts посилається на типи react-scripts та допомагає з такими речами, як імпорт SVG.

tsconfig.json

На щастя, остані версіїї шаблонів React/TypeScript самі генерують файл tsconfig.json. Однак автоматично там буде мінімум необхідних для початку налаштувань. Автор пропонує додати ще декілька конфігурацій до згенерованого файлу. Кожне налаштування супроводжується тут коментарем з поясненням:

{
  "compilerOptions": {
    "target": "es5", // Визначаємо цільову версію ECMAScript
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ], // Перелік бібліотек, які будуть додані до компіляції 
    "allowJs": true, // дозволяємо компіляцію JavaScript-файлів
    "skipLibCheck": true, //пропускаємо перевірку типів для бібліотечних файлів 
    "esModuleInterop": true, // замінюємо імпорти простору імен (import * as fs from "fs") на імпорти CJS/AMD/UMD (import fs from "fs")
    "allowSyntheticDefaultImports": true, // Дозволяємо імпорти за замовчуванням з модулів без експорту за замовчуванням 
    "strict": true, // Дозволяємо всі суворі перевірки типів 
    "forceConsistentCasingInFileNames": true, // Забороняємо непослідовні посилання на той самий файл
    "module": "esnext", // визначаємо генерацію коду модуля 
    "moduleResolution": "node", // Визначаємо модулі, використовуючи стиль Node.js 
    "resolveJsonModule": true, // Включаємо імпортовані модулі з розширенням .json 
    "noEmit": true, // Не генеруємо результат (тобто не компілюємо код, просто перевіряємо типи) 
    "jsx": "react" // Підтримуємо JSX у .tsx-файлах
    "sourceMap": true, // *** Генеруємо відповідний .map-файл ***
    "declaration": true, // *** Генеруємо відповідний .d.ts-файл ***
    "noUnusedLocals": true, // *** Повідомляємо про помилки через невикористані локальні змінні ***
    "noUnusedParameters": true, // *** Повідомляємо про помилки через невикористані параметри ***
    "experimentalDecorators": true // *** Активуємо експериментальну підтримку для ES-декораторів ***
    "incremental": true // *** Активуємо поступову компіляцію шляхом зчитування/запису інформації з попередніх компіляцій у файл/на диск ***
        "noFallthroughCasesInSwitch": true // *** Повідомляємо про помилки через Report errors for невдалі case-вирази у switch ***
  },
  "include": [
    "src/**/*" // *** файли, які потребують перевірки типів TypeScript ***
  ],
  "exclude": ["node_modules", "build"] // *** файли, які не потрібно перевіряти ***
}

Додаткові рекомендації отримано від спільноти react-typescript-cheatsheet та з офіційної документації компіляції. Це чудовий ресурс, якщо ви хочете дізнатись більше про інші налаштування та їх призначення.

ESLint/Prettier

Аби переконатись, що код відповідає правилам стилю вашої команди і що стилі узгоджені, рекомендується налаштувати ESLint і Prettier. Щоб отримати всі переваги інструментів, виконайте таке:

  • Встановіть необхідні залежності:
yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react --dev
  • Створіть файл .eslintrc.js в корені проєкту та додайте ці налаштування:
module.exports =  {
  parser:  '@typescript-eslint/parser', // Визначаємо ESLint-обробник
  extends:  [
    'plugin:react/recommended', // Використовуємо рекомендовані правила з @eslint-plugin-react
    'plugin:@typescript-eslint/recommended', //Використовуємо рекомендовані правила з @typescript-eslint/eslint-plugin
  ],
  parserOptions: {
  ecmaVersion:  2018, // Дозволяємо обробляти сучасні фічі ECMAScript 
  sourceType: 'module', // Дозволяємо використання імпортів
  ecmaFeatures: {
    jsx:  true, // Дозволяємо обробляти JSX
  },
  },
  rules: {
    // Тут ми визначаємо правила ESLint. Також можна перевизначати правила наявних конфігів
    // наприклад, "@typescript-eslint/explicit-function-return-type": "off",
  },
  settings: {
    react: {
      version: 'detect', // Вказуємо eslint-plugin-react автоматично визначати версію React для використання
    },
  },
};
  • Додаємо залежності Prettier:
yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev
  • Створюємо файл .prettierrc.js в корені проєкту та додаємо такий вміст:
module.exports = {
  semi: true,
  trailingComma: 'all',
  singleQuote: true,
  printWidth: 120,
  tabWidth: 4,
};
  • Оновлюємо файл .eslintrc.js:
module.exports = {
  parser: '@typescript-eslint/parser', // Визначаємо парсер ESLint 
  extends: [
    'plugin:react/recommended', // Використовуємо правила, рекомендовані @eslint-plugin-react
    'plugin:@typescript-eslint/recommended', // Використовуємо правила, рекомендовані @typescript-eslint/eslint-plugin
+   'prettier/@typescript-eslint', // Використовуємо eslint-config-prettier, щоб позбавитись від правил ESLint з плагіна @typescript-eslint/eslint-plugin, що може конфліктувати з prettier
+   'plugin:prettier/recommended', // Активуємо eslint-plugin-prettier та показуємо помилки prettier у вигляді помилок ESLint. Переконайтеся, що це завжди остання конфігурація в масиві extends.
  ],
  parserOptions: {
  ecmaVersion: 2018, // Дозволяємо обробку сучасних фіч ECMAScript
  sourceType: 'module', // Дозволяємо використання імпортів 
  ecmaFeatures: {
    jsx:  true, // Дозволяємо обробку JSX
  },
  },
  rules: {
    // Тут ми визначаємо правила ESLint. Також можна перевизначати правила наявних конфігів
    // наприклад, "@typescript-eslint/explicit-function-return-type": "off",
  },
  settings: {
    react: {
      version: 'detect', // Вказуємо eslint-plugin-react автоматично визначати версію React для використання
    },
  },
};

Рекомендації отримано з ком'юніті-ресурсу, вони називаються «Використання ESLint та Prettier у TypeScript-проєкті».

Розширення та налаштування VSCode

Ми вже додали ESLint та Prettier, далі налаштуємо розширення нашого редактора коду, щоб він автоматично форматував його при збереженні.

Спершу завантажимо ESLint-розширення для VSCode. Так ми зможемо плавно інтегрувати ESLint в редактор.

Далі перейдемо до налаштування робочого середовища, додавши такі конфіги до файлу .vscode/settings.json:

{
  "eslint.autoFixOnSave": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    { "language": "typescript", "autoFix": true },
    { "language": "typescriptreact", "autoFix": true }
  ],
  "editor.formatOnSave": true,
  "[javascript]": {
    "editor.formatOnSave": false
  },
  "[javascriptreact]": {
    "editor.formatOnSave": false
  },
  "[typescript]": {
    "editor.formatOnSave": false
  },
  "[typescriptreact]": {
    "editor.formatOnSave": false
  }
}

Тепер VS Code стане вашим помічником на шляху до бездоганного коду.

Описані рекомендації було запозичено з попереднього матеріалу «Використання ESLint та Prettier у TypeScript-проєкті».

Компоненти

Одна з ключових концепцій React — компоненти. Тут ми посилатимемось на стандартні компоненти React v16.8, тобто ті, що використовують хуки, на відміну від компонентів-класів.

Варто подбати про багато нюансів, використовуючи базові компоненти. Розглянемо приклад:

import React from 'react'

// Компонент у вигляді function declaration
function Heading(): React.ReactNode {
  return <h1>My Website Heading</h1>
}

// Компоненти у вигляді функціонального виразу 
const OtherHeading: React.FC = () => <h1>My Website Heading</h1>

Зверніть увагу на ключові відмінності у наведених фрагментах. Перший фрагмент демонструє функціональне оголошення. Ми позначаємо значення, що повертається як React.Node. Натомість другий приклад демонструє функціональний вираз. Оскільки поверненим значенням буде функція, ми позначаємо тип як React.FC.

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

Props

Наступна наша ключова концепція — props. Ви можете оголосити власні пропси, використовуючи інтерфейс чи тип. Розглянемо приклад:

import React from 'react'

interface Props {
  name: string;
  color: string;
}

type OtherProps = {
  name: string;
  color: string;
}

// Зверніть увагу, що ми використовуємо оголошення функції з інтерфейсом Props
function Heading({ name, color }: Props): React.ReactNode {
  return <h1>My Website Heading</h1>
}

// Зверніть увагу, що тут ми використовуємо функціональний вираз з типом OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
  <h1>My Website Heading</h1>

Коли справа доходить до типів чи інтерфейсів, автор пропонує дослухатись до кількох порад спільноти react-typescript-cheatsheet:

  • завжди використовуйте інтерфейси для публічних API під час створення бібліотек або визначень сторонніх типів зовнішніх середовищ;
  • застосовуйте типи для пропсів вашого React-компоненту та стану.

Ви можете дізнатись більше порад та розглянути відмінності між типом та інтерфейсом за посиланням.

Розглянемо ще один, більш практичний приклад:

import React from 'react'

type Props = {
  /** колір для фону */
  color?: string;
  /** стандартний дочірній проп: приймає будь-який валідний вузол React */
  children: React.ReactNode;
  /** колбек, переданий обробнику onClick */
  onClick: () => void;
}

const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
   return <button style={{ backgroundColor: color }} onClick={onClick}>{children}</button>
}

В компоненті <Button /> ми використали тип для наших props. Кожен проп містить коротке описання для розуміння іншими розробниками. Знак ? після color означає, що параметр необов'язковий.

Параметр children приймає тип React.ReactNode, тобто будь-яке валідне значення, повернене компонентом (більше за посиланням).

Враховуючи необов'язковість нашого параметра color, передаємо автоматичне значення при деструктуризації.

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

Слід пам'ятати про деякі моменти при використанні props у проєкті на React і TypeScript:

  • завжди додавайте зрозумілі коментарі для ваших props, використовуючи нотацію /** comment */;
  • незалежно від того, використовуєте ви типи чи інтерфейси для props ваших компонентів, дотримуйтесь постійності;
  • якщо параметр необов'язковий, не забувайте обробити його відсутність чи використати автоматичне значення.

Хуки

На щастя, інтерфейси TypeScript також добре працюють з хуками. Розглянемо приклад:

// тип `value` витікає з типу автоматичного значення
// тип `setValue` визначається як (newValue: string) => void
const [value, setValue] = useState('')

TypeScript автоматично визначає тип значення, переданого до хука useState. Це той випадок, коли React і TypeScript чудово співпрацюють.

У рідкісних випадках, коли вам необхідно ініціалізувати хук null, ви можете використати дженерик та передати туди перелік допустимих типів хука. Одразу приклад:

type User = {
  email: string;
  id: string;
}

// дженериком називається < >
// об'єднанням називається User | null
// тобто TypeScript розуміє, що змінна user може бути типу User чи null.
const [user, setUser] = useState<User | null>(null);

Інший приклад узгодженої роботи TypeScript і хуків: userReducer, де використовується перевага розмічених об'єднань. Розглянемо корисний приклад:

type AppState = {};
type Action =
  | { type: "SET_ONE"; payload: string }
  | { type: "SET_TWO"; payload: number };

export function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "SET_ONE":
      return {
        ...state,
        one: action.payload // `payload` типу рядок
      };
    case "SET_TWO":
      return {
        ...state,
        two: action.payload // `payload` типу число
      };
    default:
      return state;
  }
}

Джерело: react-typescript-cheatsheet розділ Хуків

Уся краса тут в корисності розмічених об'єднань. Зверніть увагу, Action визначається як об'єднання двох схожих об'єктів. Властивість typeрядковий літерал. Відмінність від типу string у тому, що значення повинно відповідати літералу рядка, визначеного в типі. Тобто ваш застосунок буде супербезпечним, адже розробник може лише викликати action з параметром type зі значенням "SET_ONE" або "SET_TWO".

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

Поширені варіанти використання

Далі поговоримо про найбільш поширені задачі, що спричиняють труднощі у розробників, коли вони використовують TypeScript разом з React. Сподіваємось, що інформація далі дозволить вам оминати ці перешкоди.

Обробка подій форми

Одна з найбільш поширених задач — коректне використання обробника onChange при зміні поля форми. Розглянемо приклад:

import React from 'react'

const MyInput = () => {
  const [value, setValue] = React.useState('')

  // Тип події – "ChangeEvent"
  // В дженерик ми передаємо тип "HTMLInputElement" 
  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    setValue(e.target.value)
  }

  return <input value={value} onChange={onChange} id="input-example"/>
}

Розширення props-компонента

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

Добре, пригадаймо, як ми розглядали два способи типізувати параметри компонента: користувацькі типи та інтерфейси. Спосіб типізації визначає механізм розширення параметрів. Спочатку з'ясуємо, як працювати з type:

import React from 'react';

type ButtonProps = {
    /** фоновий колір кнопки */
    color: string;
    /** текст всередині кнопки */
    text: string;
}

type ContainerProps = ButtonProps & {
    /** висота контейнера (значення використовується в пікселях) */
    height: number;
}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}

Якщо ви оголосили ваші props за допомогою interface. Потім ви можете використовувати ключове слово extends аби розширити інтерфейс й внести декілька модифікацій:

import React from 'react';

interface ButtonProps {
    /** фоновий колір кнопки */
    color: string;
    /** текст всередині кнопки */
    text: string;
}

interface ContainerProps extends ButtonProps {
    /** висота контейнера (значення використовується в пікселях) */
    height: number;
}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}

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

Ви можете заглибитись в обидві концепції за посиланнями:

Сторонні бібліотеки

Неважливо, чи ми використовуємо сторонні бібліотеки у проєктах на React і TypeScript для GraphQL-клієнта на зразок Apollo, чи для тестування з React Testing Library. Коли справа доходить до сторонніх бібліотек, перше, на що варто звернути увагу, — пакет @types з оголошенням типів TypeScript. Ви можете знайти його командою:

#yarn
yarn add @types/<package-name>

#npm
npm install @types/<package-name>

Наприклад, якщо ви використовуєте Jest, команда буде такою:

#yarn
yarn add @types/jest

#npm
npm install @types/jest

Так ви точно будете спокійними щодо типів при використанні бібліотеки у своєму проєкті.

Простір імен @types зарезервовано для оголошень типу пакета. Вони розташовані в репозиторії DefinitelyTyped, який частково підтримується спільнотою TypeScript, а частково — ком'юніті.

Зберігати типи треба як dependencies чи devDependencies в package.json?

Коротка відповідь: все залежить від потреб. У більшості випадків краще обрати devDependencies, якщо ви створюєте веб-застосунок. Однак вам можуть знадобитися dependencies,якщо ви пишете React-бібліотеку на TypeScript .

За детальною інформацією зверніться до відповідей на Stack Overflow.

Що робити, якщо немає пакета @types?

Якщо ви не знайшли @types на npm, у вас є такі варіанти:

  • Додати базовий файл оголошень;
  • Додати заглиблений файл оголошень.

Перший варіант означає, що ви створюєте файл, який базується на назві пакета, та розміщуєте його в корені. Якщо, наприклад, вам потрібні типи для пакета banana-js, треба створити базовий файл оголошень banana-js.d.ts в корені:

declare module 'banana-js';

Звичайно, це не убезпечить ваш проєкт, однак не буде затримувати розробку.

Більш поглиблене оголошення полягає в тому, що ви додаєте типи для бібліотеки/пакета:

declare namespace bananaJs {
    function getBanana(): string;
    function addBanana(n: number) void;
    function removeBanana(n: number) void;
}

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

Що далі?

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

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.3K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація