Нещодавно я рефакторив компонент React, який був класичним прикладом надмірної складності коду. Він чудово обробляв API виклики, рендеринг UI та складну логіку, але все це в одному файлі. Результат? … 🥺Мені знадобилося години, щоб зрозуміти кожен компонент (який було просто неможливо зрозуміти) або змінити. Я пам’ятаю свої перші дні вивчення React, коли переглядав цей код. За допомогою кількох простих найкращих практик і принципів, я перетворив це на набір чистих, повторно використовуваних компонентів, що покращило підтримуваність кодової бази.
Ці принципи обговорюються на етапі початкового навчання, і я думав, що вони мають глибоке коріння в моїх основах перед початком написання коду, а також допомагають мені вирішувати проблеми в складному коді.
Я маю 7 правил/принципів, яких дотримуюсь для свого коду. Я б хотів поділитися ними тут, щоб інші могли навчитися:
- Принцип єдиного обов'язку (SRP) (👍 основне правило)
- Використання hooks у функціональних компонентах
- Хардкодинг і імена
- Уникати прямої мутації стану
- Зменшення непотрібних повторних рендерів: мемоізація в React
- Валідація props за допомогою PropTypes або TypeScript
- Документуйте та коментуйте ваші компоненти
1. Принцип єдиного обов'язку (SRP):
Я згадую цей SRP за допомогою прикладу з світлофорами. Але як це пов'язано? Дозвольте пояснити SRP, щоб ви зрозуміли, чому я згадую його таким чином.
Принцип єдиного обов'язку (SRP) стверджує, що клас або компонент має один обов'язок, і цей обов'язок повинен бути інкапсульований у цьому класі або компоненті. Для спрощення кожен компонент повинен мати одну чітку мету і бути відповідальним лише за один аспект інтерфейсу користувача, як і світлофори. Уявіть світлофор на перехресті. Його єдиний обов'язок — це регулювати потік транспорту:
- Сигналізувати, коли зупинятися (червоне світло)
- Сигналізувати, коли рухатись з обережністю (жовте світло)
- Сигналізувати, коли можна їхати (зелене світло)
Кожне світло має конкретну, незалежну функцію, пов'язану з потоком транспорту.
Давайте зрозуміємо важливість SRP і наслідки відсутності SRP на коді за допомогою простих прикладів.
- Без SRP складність коду збільшується, і компоненти беруть на себе більше обов'язків, що призводить до перевантаження.
- Збільшується час для читабельності та усунення неполадок.
- Масштабованість — це ще одна важлива характеристика, яка може бути втрачена, якщо ви не використовуєте SRP. Ми не можемо масштабувати наш код, додаючи нові функції, інтегруючи сторонні інструменти або змінюючи існуючі функції без порушення 😢 коду.
Приклад SRP
Без SRP
function TaskCard({ taskId }) {
const [task, setTask] = useState(null);
// Отримання даних завдання
useEffect(() => {
// API виклик
}, [taskId]);
// Оновлення статусу завдання
const markAsComplete = () => {
fetch(`https://api.example.com/tasks/${taskId}/complete`, { method: "POST" })
.then(() => alert("Завдання позначено як завершене!"));
};
return (
{task?.title}
{task?.description}
Complete Task
); }
З SRP
// Користувальницький hook для отримання даних
function useTask(taskId) {
const [task, setTask] = useState(null);
useEffect(() => {
// API виклик
}, [taskId]);
return task;
}
// Компонент TaskCard
function TaskCard({ taskId }) {
const task = useTask(taskId);
const markAsComplete = () => {
fetch(`https://api.example.com/tasks/${taskId}/complete`, { method: "POST" })
.then(() => alert("Завдання позначено як завершене!"));
};
return (
{task?.title}
{task?.description}
Complete Task
);
}
2.
Використання hooks у функціональних компонентах:
Перед тим, як зрозуміти цей принцип, давайте розглянемо, що таке класові та функціональні компоненти.
Класові компоненти — це класи JavaScript, які розширюються від React.Component
. Вони мають метод render()
, який повертає JSX.
Функціональні компоненти — це прості функції JavaScript, які приймають props
(властивості) як аргумент і повертають JSX (JavaScript XML), що описує, що повинно відображатися на екрані.
З вище наведеного ми можемо зрозуміти, що функціональні компоненти приймають props
і за допомогою hooks вони керують станом (наприклад, useState
) і життєвим циклом (наприклад, useEffect
). Це робить їх більш лаконічними в коді. Вони дуже прості для вивчення порівняно з класовими компонентами.
У класових компонентах стан керується за допомогою this.state
, а життєвий цикл — за допомогою методів життєвого циклу (наприклад, componentDidMount
), що робить їх складнішими та застарілими.
3. Хардкодинг і імена:
Це одна з причин, чому я написав цю статтю, тому що останній код, який я рефакторив, містив багато хардкодених значень і неправильних конвенцій іменування.
Уявіть, що ви переглядаєте вебсайт, на якому хардкодується колір фону та тексту. Чи зможемо ми прочитати цей сайт при різних умовах освітлення?
Уявіть, що ви стоїте перед скляними дверима, на яких написано "push" з одного боку і "pull" з іншого 😆. Ми в кінцевому підсумку розіб'ємо ніс 😵 через неправильні або оманливі написи на дверях.
Тому в будь-якому коді завжди краще підтримувати динамічність коду замість хардкодування та використовувати відповідні імена для функцій, змінних та класів тощо.
В React camelCase є стандартною і рекомендованою конвенцією іменування для кількох ключових елементів. Ви також можете використовувати Pascal Case (наприклад, MyComponent
, UserProfile
, ProductDetails
), UPPERSNAKECASE (наприклад, API_KEY
, MAX_VALUE
, DEFAULT_COLOR
) або kebab-case (наприклад, my-component
, user-profile
, product-details.css
)
4. Уникати прямої мутації стану
Пряма мутація змінних стану зазвичай вважається антипатерном в багатьох підходах до управління станом.
Існує кілька найкращих практик для мутації стану. Деякі з них нижче:
- Використання незмінних оновлень: Коли оновлюєте стан, завжди краще створювати новий об'єкт або масив, а не змінювати існуючий. Це гарантує, що React може правильно виявити зміни і викликати повторний рендер.
Приклад:
// Неправильно (пряма мутація)
const [user, setUser] = useState({ name: 'John', age: 30 });
user.age = 31; // Не робіть цього!
// Правильно (незмінне оновлення)
setUser({ ...user, age: 31 });
2. Використання функції оновлення стану: Цей метод головним чином рекомендується для функціональних компонентів. За допомогою useState
ви отримуєте функцію встановлення стану. Тому завжди використовуйте цю функцію для оновлення стану.
Приклад:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
// Правильно: використовуйте функцію встановлення стану
setCount(prevCount => prevCount + 1); // Використовуйте функціональні оновлення для правильного попереднього стану
// АБО
//setCount(count + 1); // Це також правильно, якщо не залежить від попереднього стану
// Неправильно: пряма мутація (НЕ РОБІТЬ ЦЬОГО)
// count++; // Це не викличе повторний рендер
};
return (
Count: {count} Increment
);
}
3. Правильне використання setState
для класових компонентів: У класових компонентах використовуйте this.setState()
.
Ніколи не змінюйте this.state
безпосередньо.
Приклад:
import React from 'react';
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => {
// Правильно: використовуйте setState
this.setState(prevState => ({ count: prevState.count + 1 })); // Використовуйте функціональні оновлення для правильного попереднього стану
// АБО
//this.setState({ count: this.state.count + 1 }); //Це також правильно, якщо не залежить від попереднього стану
// Неправильно: пряма мутація (НЕ РОБІТЬ ЦЬОГО)
// this.state.count++; // Це не викличе повторний рендер
};
render() {
return (
Count: {this.state.count} Increment
);
}
}
3. Створення нових об'єктів/масивів при оновленні стану: Коли працюєте з об'єктами або масивами в стані, не змінюйте їх безпосередньо. Замість цього створюйте новий об'єкт або масив з внесеними змінами.
Для об'єктів: Використовуйте синтаксис розповсюдження (...
), щоб створити поверхневу копію.
Приклад:
const [user, setUser] = useState({ name: 'Alice', age: 30 });
const updateName = () => {
// Правильно: створіть новий об'єкт
setUser({ ...user, name: 'Bob' });
// Неправильно: пряма мутація
// user.name = 'Bob'; // Це Погано
// setUser(user) // Це також Погано
};
Для масивів: Використовуйте методи як map
, filter
, slice
або синтаксис розповсюдження для створення нових масивів.
Приклад:
const [items, setItems] = useState([1, 2, 3]);
const addItem = (newItem) => {
// Правильно: створіть новий масив
setItems([...items, newItem]); // або setItems(items.concat(newItem));
// Неправильно: пряма мутація
// items.push(newItem); // Це Погано
// setItems(items) // Це також Погано
};
const removeItem = (indexToRemove) => {
// Правильно: створіть новий масив
setItems(items.filter((_, index) => index !== indexToRemove))
}
5. Зменшення непотрібних повторних рендерів: Мемоізація в React:
Мемоізація — потужна техніка оптимізації, що дозволяє зменшити непотрібні повторні рендери та покращити продуктивність додатка. Процес передбачає кешування результатів дорогих обчислень або рендерів компонентів і повторне їх використання, коли вхідні дані не змінюються.
Використовуйте вбудовані інструменти:
Використовуйте вбудовані інструменти React, такі як React.memo
, щоб мемоізувати компоненти, useMemo
для кешування значень і useCallback
для мемоізації функцій. Ці інструменти допомагають уникнути непотрібних перерахунків і повторних рендерів.
Визначення залежностей:
Чітко визначайте залежності в hooks для мемоізації (наприклад, useMemo
або useCallback
). React буде перераховувати або перерендерювати лише тоді, коли ці залежності змінюються, забезпечуючи ефективні оновлення.
Оптимізація рендерів:
Пропускаючи повторні рендери для незмінних залежностей, React підвищує продуктивність, особливо в додатках з складними компонентами або дорогими обчисленнями.
Приклад: Мемоізація компонента
Розглянемо простий приклад компонента Post
, який відображає блог-пост:
import React, { useState, useEffect } from 'react';
const Post = ({ id, title }) => {
console.log(`Рендеринг посту ${id}`);
const [content, setContent] = useState('');
useEffect(() => {
// Симуляція дорогого API виклику
const fetchContent = async () => {
// API виклик і встановлення даних
};
fetchContent();
}, [id]);
return (
<div>
<h1>{title}</h1>
<p>{content}</p>
</div>
);
};
export default React.memo(Post);
Розглянемо, що ми зробили:
1. Обгорнули компонент Post
у React.memo()
, щоб мемоізувати його.
2. React.memo
запобіжить повторному рендеру компонента, якщо його props (id
і title
) не зміняться.
3.
Хук useEffect
забезпечує, що вміст буде завантажено лише тоді, коли змінюється id
, запобігаючи непотрібним API викликам.
Батьківський компонент:
const BlogPage = () => {
const [posts, setPosts] = useState([]);
const [counter, setCounter] = useState(0);
useEffect(() => {
// Отримання постів
setPosts([
{ id: 1, title: 'React Memoization' },
{ id: 2, title: 'Optimizing React Apps' }
]);
}, []);
return (
Blog Posts
setCounter(counter + 1)}> Click me: {counter} {posts.map(post => ( ))}
);
};
Розберемо це:
- Компонент
BlogPage
рендерить список компонентівPost
. - Є кнопка лічильника, яка оновлює стан
counter
. - Без мемоізації кожен компонент
Post
буде перерендерюватися, коли змінюєтьсяcounter
, навіть якщо props для постів залишаються тими самими. - За допомогою
React.memo
компонентиPost
перерендерюються лише тоді, коли змінюються їх props (id
абоtitle
), а не коли оновлюєтьсяcounter
.
6. Валідація props за допомогою PropTypes або TypeScript:
Як PropTypes, так і TypeScript використовуються в React для валідації props. Валідація props гарантує, що дані, передані в компонент (через props), мають очікуваний тип. Це допомагає виявляти помилки на ранніх етапах розробки та покращує підтримуваність коду.
PropTypes — це інструмент React для перевірки типів props під час виконання, що показує попередження в консолі для неправильних типів.
Приклад:
import React from "react";
import PropTypes from "prop-types";
function Button({ label, onClick, isDisabled }) {
return (
{label}
);
}
// Валідація props
Button.propTypes = {
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
isDisabled: PropTypes.bool,
};
Button.defaultProps = {
isDisabled: false,
};
export default Button;
Зміст:
PropTypes.string
гарантує, щоlabel
є рядком.PropTypes.func
гарантує, щоonClick
є функцією.PropTypes.bool
гарантує, щоisDisabled
є булевим значенням (необов’язковим за допомогоюdefaultProps
як резервного варіанту).
TypeScript — це супerset JavaScript з статичною типізацією, що надає перевірки під час компіляції, кращі інструменти (автодоповнення, виведення типів) і кращу безпеку типів порівняно з PropTypes.
Приклад:
import React from "react";
type ButtonProps = {
label: string;
onClick: () => void;
isDisabled?: boolean;
};
const Button: React.FC<ButtonProps> = ({ label, onClick, isDisabled = false }) => {
return (
{label}
);
};
export default Button;
Зміст:
label: string
гарантує, щоlabel
є рядком.onClick: () => void
гарантує, щоonClick
є функцією без значення, яке повертається.isDisabled?: boolean
означає, щоisDisabled
є необов’язковим і булевим значенням.
Основні відмінності
7. Документуйте та коментуйте ваші компоненти:
Правильна документація забезпечує та допомагає повторному використанню компонентів, роз'яснюючи їх призначення, props та очікувану поведінку, що приносить користь як вам, так і іншим.
Переваги ефективної документації компонентів:
-
Оптимізація співпраці:
Покращує розуміння команди, пришвидшує розробку та спрощує вхід нових членів команди. -
Легше обслуговування та налагодження:
Чіткі коментарі економлять час розробки, спрощуючи розуміння поведінки компонентів, налагодження проблем і безпечне внесення змін. -
Краща повторна використаність:
Чітка документація props і виводів компонента дозволяє будь-якому розробнику легко зрозуміти та інтегрувати його в проект.
Покращення розробки з безпекою типів та автоматично згенерованими документами
- Покращуйте ваш робочий процес! 🚀 Поєднуючи коментарі з PropTypes або TypeScript, ваш IDE і лінтери набувають суперсил.
Вони забезпечують інтелектуальне автодоповнення коду, виявляють помилки на ранніх етапах та пропонують безцінні рекомендації, що заощаджує ваш час і зусилля.
- Використовуйте коментарі JSDoc-style, TypeScript, TypeDoc та EsDoc для автоматичного створення всеохоплюючої та актуальної документації для ваших компонентів React, що гарантує синхронізацію вашої кодової бази та її документації.
Документування компонентів React: практичний приклад
/**
* Компонент Button
* @param {string} label - Текст, який відображається на кнопці.
* @param {function} onClick - Callback-функція, яка викликається при натисканні на кнопку.
* @param {string} [type="button"] - Тип кнопки (наприклад, "button", "submit", "reset").
* @returns {JSX.Element} Налаштовуваний компонент кнопки.
*/
function Button({ label, onClick, type = "button" }) {
return (
{label}
);
}
Button.propTypes = {
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
type: PropTypes.string,
};
export default Button;
Роздумаючи про ваші поточні практики кодування, визначте області для покращення. Почніть з реалізації одного або двох із цих принципів у вашому наступному проекті та поступово впроваджуйте їх у ваш робочий процес. Пам'ятайте, що чистий код — це не просто написання коду, який працює; це написання коду, який легко зрозуміти, підтримувати та розширювати.
Чи був корисним цей TechBlog? Лайки та підписки допомагають мені створювати більше цінного контенту. Зв'яжіться зі мною через електронну пошту: [email protected]
Перекладено з: Give Your React Code a Makeover with Reusable Components