У каждого разработчика, как освоившего React, так и начинающего изучать этот инструмент, всегда есть возможность оптимизировать рабочий процесс и повысить эффективность кода.
Вот 25 советов, которые помогут улучшить React-код.
1. Избегайте объявления/вкладывания компонентов внутрь других компонентов
Такая практика, хотя поначалу может показаться удобной, чревата серьезными проблемами с производительностью в приложении.
Каждый раз, когда родительский компонент рендерится повторно из-за обновления состояния, вложенный компонент создается заново. Это означает, что предыдущее состояние и данные, объявленные во вложенном компоненте, теряются, что вызывает серьезные проблемы с производительностью.
// Неправильно:
const UsersList = () => {
❌ Declaring User component inside UsersList component
const User = ({ user }) => {
// код
return <div>{/* User Component JSX */}</div>
};
return <div>{/* UsersList JSX */}</div>
};
// Правильно:
✅ Declaring User component outside UsersList component
const User = ({ user }) => {
// код
return <div>{/* User Component JSX */}</div>
};
const UsersList = ({ users }) => {
// код
return <div>{/* UsersList component JSX */}</div>
};
2. Корректно используйте массив зависимостей в useEffect
Всегда включайте каждую внешнюю переменную, на которую ссылается useEffect
, в массив зависимостей []
, чтобы избежать ошибок, вызванных устаревшими замыканиями.
useEffect(() => {
const fetchData = (id) => {
// код для получения данных
}
id && fetchData(id);
}, [id]);
3. Применяйте ленивую инициализацию состояния с помощью useState
Для дорогостоящих вычислений начального состояния можете отложить оценку до рендеринга компонента, передав функцию useState
.
Таким образом, функция будет запущена только один раз во время начального рендеринга компонента.
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
// ленивая инициализация состояния
try {
const localValue = window.localStorage.getItem(key);
return localValue ? JSON.parse(localValue) : initialValue;
} catch (error) {
return initialValue;
}
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
4. Мемоизируйте дорогостоящие функции с помощью useMemo
Для дорогостоящих вычислений, которые не нужно выполнять при каждом рендеринге, используйте хук useMemo
, чтобы запомнить результат и повысить производительность.
Первый фрагмент кода, показанный ниже, вызывает проблемы с производительностью, потому что при каждом повторном рендеринге компонента массив users
фильтруется и сортируется снова и снова.
И если массив users
содержит много данных, то это приведет к замедлению работы приложения.
Но если использовать хук useMemo
, то массив users
будет фильтроваться и сортироваться только при изменении зависимости users
.
// Вместо того чтобы писать код следующим образом:
function App({ users }) {
return (
<ul>
{users
.map((user) => ({
id: user.id,
name: user.name,
age: user.age,
}))
.sort((a, b) => a.age - b.age)
.map(({ id, name, age }) => {
return (
<li key={id}>
{name} - {age}
</li>
);
})}
</ul>
);
}
// пишите вот так:
function App({ users }) {
const sortedUsers = useMemo(() => {
return users
.map((user) => ({
id: user.id,
name: user.name,
age: user.age,
}))
.sort((a, b) => a.age - b.age);
}, [users]);
return (
<ul>
{sortedUsers.map(({ id, name, age }) => {
return (
<li key={id}>
{name} - {age}
</li>
);
})}
</ul>
);
}
5. Создавайте пользовательские хуки для многоразовой логики
Пользовательские хуки — мощный способ извлечения из компонентов логики многократного использования. Если приходится копировать и вставлять один и тот же код в несколько компонентов, создайте пользовательский хук.
function useFetch(url) { // пользовательский хук для получения данных из API путем передачи url
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => setData(data));
}, [url]);
return data;
}
6. Используйте Фрагменты для избежания лишних узлов DOM
Добавление ненужных элементов <div>
к компонентам может привести к раздутой DOM. Использование Фрагментов обеспечивает чистоту DOM и оптимизирует производительность.
Учтите, что каждый раз при добавлении нового <div>
, React должен создавать этот элемент, используя React.createElement
. Поэтому избегайте создания ненужных <div>
.
// Вместо такого стиля написания кода:
return (
<div>
<Child1 />
<Child2 />
</div>
);
// используйте этот:
return (
<>
<Child1 />
<Child2 />
</>
);
// или этот:
return (
<React.Fragment>
<Child1 />
<Child2 />
</React.Fragment>
);
7. Применяйте составные компоненты для создания гибких UI
Составные компоненты помогают создавать гибкие UI, позволяя компонентам неявно взаимодействовать между собой.
Это отличный способ создания многократно используемых и легко настраиваемых компонентов.
function Tab({ children }) {
return <div>{children}</div>;
}
Tab.Item = function TabItem({ label, children }) {
return (
<div>
<h3>{label}</h3>
<div>{children}</div>
</div>
);
};
// Использование:
<Tab>
<Tab.Item label="Tab 1">Content 1</Tab.Item>
<Tab.Item label="Tab 2">Content 2</Tab.Item>
</Tab>
8. Всегда добавляйте свойство key при рендеринге с помощью метода map
Чтобы помочь React оптимизировать рендеринг, всегда добавляйте уникальный параметр key
к элементам, выводимым в списке.
Значение key
должно оставаться неизменным и не должно меняться при повторном рендеринге компонента.
Всегда следует иметь уникальный идентификатор для каждого элемента массива, который может быть использован в качестве key
.
Избегайте использования индекса массива в качестве значения для key
. Использование индекса массива в качестве свойства key
может привести к неожиданному поведению UI при добавлении, удалении или переупорядочивании элементов массива.
// ✅ Используйте уникальный ID в качестве key
{items.map(item => (
<Item key={item.id} item={item} />
))}
// ❌ Не используйте индекс в качестве key
{items.map((item, index) => (
<Item key={index} item={item} />
))}
9. Используйте Error Boundary, чтобы избежать сбоев в работе приложения в продакшне
Error Boundary в React — мощный механизм для отлова и изящной обработки ошибок, возникающих во время рендеринга компонентов.
Использование Error Boundary помогает предотвратить нарушение работы всего приложения, изолируя ошибки от конкретных компонентов и позволяя остальным компонентам продолжать работу.
<ErrorBoundary
FallbackComponent={ErrorPage}
onReset={() => (location.href = '/')}
>
<App />
</ErrorBoundary>
10. Применяйте Context вместо объявления свойства вверху дерева компонентов с пробросом до места использования
Вместо того чтобы передавать свойства через множество вложенных компонентов, используйте React Context API для более управляемого обмена данными.
Лучший пример применения: совместное использование глобального состояния, например темы, пользовательских данных или настроек.
export const AuthContext = React.createContext();
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const updateUser = (user) => {
setUser(user);
};
return (
<AuthContext.Provider value={{ user, updateUser }}>
{children}
</AuthContext.Provider>
);
};
// Использование:
<AuthContextProvider>
<App />
</AuthContextProvider>
11. Используйте хук useRef для сохранения значения при разных рендерингах
Вместо хука useState
для хранения значения используйте хук useRef, если не хотите повторно рендерить компонент при установке значения.
Пример применения: хранение тайм-аутов, интервалов, DOM-ссылок или любых других значений, которые не должны вызывать повторный рендеринг, чтобы избежать проблем с замыканием.
const interval = useRef(-1);
useEffect(() => {
interval.current = setInterval(() => {
console.log('Timer executed');
}, 1000);
return () => clearInterval(interval.current);
}, []);
12. Разделяйте React-код с помощью Suspense
Suspense в React позволяет разделить код на более мелкие фрагменты, повышая производительность за счет загрузки компонентов только при необходимости.
import { Suspense, lazy } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/dashboard'));
const Profile = lazy(() => import('./pages/profile'));
const App = () => {
return (
<BrowserRouter>
<Suspense fallback={<p>Loading...</p>}>
<Routes>
<Route path='/dashboard' element={<Dashboard />} />
<Route path='/profile' element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
};
Код компонентов Profile
и Dashboard
будет загружаться не изначально, а только при обращении к маршруту /profile
или /dashboard
соответственно.
13. Выявляйте потенциальные проблемы в режиме StrictMode
Включите режим StrictMode
в процессе React-разработки, чтобы выявить потенциальные проблемы в приложении, такие как небезопасные жизненные циклы, устаревшие методы и неожиданные побочные эффекты.
Включение StrictMode
отображает предупреждение в консоли браузера, что помогает избежать ошибок в будущем.
<React.StrictMode>
<App />
</React.StrictMode>
14. Управляйте состоянием локально, прежде чем поднимать его наверх
Избегайте излишнего поднятия состояния в родительские компоненты.
Если часть состояния используется только одним компонентом или тесно связанными компонентами, сохраняйте ее локально, чтобы не усложнять управление состоянием.
Ведь при изменении состояния в родительском компоненте все прямые и косвенные дочерние компоненты будут подвергнуты повторному рендерингу.
function ChildComponent() {
const [value, setValue] = useState(0);
return <div>{value}</div>;
}
15. Предпочитайте функциональные компоненты компонентам классов
Всегда используйте функциональные компоненты с хуками вместо компонентов классов.
Функциональные компоненты обладают более лаконичным синтаксисом, лучшей читаемостью и позволяют легче управлять побочными эффектами.
Они также немного быстрее компонентов классов, поскольку не имеют наследуемых методов жизненного цикла и дополнительного кода родительских наследуемых классов.
function MyComponent() {
const [state, setState] = useState(0);
return <div>{state}</div>;
}
16. Используйте IntersectionObserver для ленивой загрузки элементов
Используйте Intersection Observer API для загрузки изображений, видео или других элементов только тогда, когда они видны на экране.
Это повышает производительность, особенно в случае работы с длинными списками или приложениями с большим количеством медиафайлов.
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Загрузка изображения или других данных
}
});
});
observer.observe(elementRef.current);
}, []);
17. Разбивайте большие компоненты на более мелкие
Разбивание больших компонентов на более мелкие, пригодные для повторного использования, позволяет улучшить читаемость и сопровождаемость.
Стремитесь к компонентам, которые следуют принципу «единой ответственности».
Небольшие компоненты легче тестировать, поддерживать и понимать.
const Header = () => <header>{/* Header Component Code */}</header>;
const Footer = () => <footer>{/* Footer Component Code */}</footer>;
18. Избегайте ненужных вызовов useEffect и setState
Помните, что код, написанный в хуке useEffect
, всегда будет выполняться после цикла рендеринга, а вызов setState
внутри useEffect
приводит к повторному рендерингу компонента.
Кроме того, если родительский компонент рендерится, то все дочерние компоненты также будут рендериться. Чтобы избежать ненужных повторных рендерингов, всегда старайтесь минимизировать количество лишних хуков useEffect
и по возможности избегать избыточных вызовов setState
.
// Вместо такого написания кода:
const [username, setUsername] = useState("");
const [age, setAge] = useState("");
const [displayText, setDisplayText] = useState("");
useEffect(() => {
setDisplayText(`Hello ${username}, your year of birth is ${new Date().getFullYear() - age}` );
}, [username, age]);
// пишите так:
const [username, setUsername] = useState("");
const [age, setAge] = useState("");
// создание локальной переменной в компоненте
const displayText = `Hello ${username},
your year of birth is ${new Date().getFullYear() - age}`;
С помощью приведенного выше кода displayText
всегда будет обновляться при изменении username
(имени пользователя) или age
(возраста), что позволит избежать лишних повторных рендерингов компонента.
19. По возможности избегайте написания дополнительных функций в компонентах
Каждая функция, объявленная внутри функционального компонента, создается заново при каждом повторном рендеринге компонента.
Допустим, у вас есть объявленные отдельные функции для показа/скрытия модала, который манипулирует состоянием, например так:
const [isOpen, setIsOpen] = React.useState(false);
function openModal() {
setIsOpen(true);
}
function closeModal() {
setIsOpen(false);
}
Тогда можно упростить эту логику, создав единственную функцию toggleModal
, которая будет управлять как открытием, так и закрытием модала:
const [isOpen, setIsOpen] = React.useState(false);
function toggleModal() {
setIsOpen((open) => !open);
}
Таким образом, просто вызвав функцию toggleModal
, можно будет закрыть модал, если он открыт, и наоборот.
20. Всегда присваивайте начальное значение хуку useState
Каждый раз, объявляя состояние в функциональном компоненте, присваивайте начальное значение, например так:
// установка начального значения пустого массива
const [pageNumbers, setPageNumbers] = useState([]);
Никогда не объявляйте состояние без значения по умолчанию, как в этом случае:
const [pageNumbers, setPageNumbers] = useState();
Это затрудняет понимание того, какое значение нужно присвоить при наличии множества состояний и попытке установить состояние с помощью функции setPageNumbers
позже в коде.
Если вы работаете в команде, один разработчик может присвоить массив в одном месте, используя setPageNumbers([1, 2, 3, 4])
, а другой — число, например setPageNumbers(20)
.
Если вы не используете TypeScript, это может создать трудноотлаживаемую проблему, которая возникнет только из-за того, что вы не укажете начальное значение.
Поэтому всегда указывайте начальное значение при объявлении состояния.
21. Учитывайте условия обновления состояния и немедленного доступа в React
В React важно помнить, что состояние не обновляется сразу после его установки. Это означает, что доступ к значению состояния сразу после его установки не обеспечивает получение обновленного значения.
Рассмотрим для примера код, в котором мы попытаемся получить обновленное состояние сразу после вызова setIsOnline
.
const [isOnline, setIsOnline] = React.useState(false);
function toggleOnlineStatus() {
setIsOnline(!isOnline);
console.log(isOnline); // В лог выводится старое, а не обновленное значение
}
То же самое происходит и с приведенным ниже кодом:
const [isOnline, setIsOnline] = React.useState(false);
function doSomething() {
if(isOnline){ // Значение isOnline все еще не обновлено
// какие-либо действия
}
}
function toggleOnlineStatus() {
setIsOnline(!isOnline);
doSomething();
}
React обновляет состояние во время следующего рендеринга, после того как выполнит весь код текущей функции.
Поэтому в приведенной выше функции doSomething
значение isOnline
будет по-прежнему старым, а не обновленным.
22. Избегайте использования Context API во всех случаях
Передача свойств компонентам глубиной в один или два уровня не является проблемой. Свойства предназначены для передачи компонентам, поэтому нет необходимости использовать Context API только для того, чтобы не передавать эти свойства.
Многие React-разработчики используют Context API только для того, чтобы избежать передачи свойств, но это не совсем то, что нужно. Context API следует использовать только тогда, когда вы хотите избежать так называемого «prop drilling» (объявления свойства вверху дерева компонентов с пробросом до места использования), что означает передачу свойств глубоко вложенным компонентам.
23. Всегда используйте null в качестве значения для хранения объектов в состоянии
Когда нужно сохранить объект в состоянии, используйте null
в качестве начального значения, как показано ниже:
const [user, setUser] = useState(null);
А не пустой объект, как здесь:
const [user, setUser] = useState({});
Поэтому, чтобы проверить, существует ли user
или есть ли у него какие-либо свойства, используйте простое условие if, как здесь:
if (user) {
// какие-либо действия
}
Или используйте JSX-выражение, как здесь:
{ user && <p>User found</p>}
Однако при использовании пустого объекта в качестве начального значения проверить, существует ли user
, нужно следующим образом:
if(Object.keys(user).length === 0) {
// какие-либо действия
}
Или используйте JSX-выражение, например так:
{ Object.keys(user).length === 0 && <p>User found</p>}
Поэтому присвоение начального значения null
сделает код более простым и понятным.
24. Всегда создавайте компонент ContextProvider вместо прямого использования Context.Provider
При использовании React Context API не передавайте свойство value
напрямую Provider
, как показано ниже:
const App = () => {
....
return (
<AuthContext.Provider value={{ user, updateUser }}>
<ComponentA />
<ComponentB />
</AuthContext.Provider>
);
}
В приведенном выше коде каждый раз, когда компонент App
будет повторно рендериться из-за обновления состояния, свойство value
, переданное AuthContext.Provider
, будет меняться.
Из-за этого все прямые и косвенные компоненты, находящиеся между открывающим и закрывающим тегом AuthContext.Provider
, будут повторно рендериться без необходимости.
Поэтому всегда создавайте отдельный компонент, как показано ниже:
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState(null);
const updateUser = (user) => {
setUser(user);
};
return (
<AuthContext.Provider value={{ user, updateUser }}>
{children}
</AuthContext.Provider>
);
};
И используйте его, как показано ниже:
<AuthContextProvider>
<ComponentA />
<ComponentB />
</AuthContextProvider>
В React children
— специальное свойство, поэтому даже если родительский компонент будет рендериться повторно, любой компонент, переданный в качестве свойства children
, не будет подвергнут повторному рендерингу.
Поэтому, если пишете код следующим образом:
<AuthContextProvider>
<ComponentA />
<ComponentB />
</AuthContextProvider>
В таком случае компоненты, обернутые между открывающим и закрывающим тегом AuthContextProvider
, будут рендериться повторно, только если используется хук useContext
. В противном случае они не будут рендериться повторно даже при повторном рендеринге родительского компонента.
Поэтому, чтобы избежать ненужного повторного рендеринга всех дочерних компонентов, всегда создавайте отдельный компонент для обертывания свойства children
.
25. Избегайте непреднамеренного рендеринга с помощью логического AND (&&) в JSX
Не совершайте ошибку при написании JSX в таком виде:
{ users.length && <User /> }
Если значение users.length
равно нулю (как в приведенном выше коде), то в итоге на экран выведется 0
, потому что логический оператор &&
(также известный как оператор замыкания) мгновенно возвращает это значение, если оно оценивается как ложное.
Таким образом, приведенный выше код будет эквивалентен приведенному ниже коду, если users.length
равно 0
.
{ 0 && <User /> }
А это оценивается как {0}
.
Вместо этого можно использовать один из следующих способов корректной записи JSX:
→ { users.length > 0 && <User /> }
→ { !!users.length && <User /> }
→ { Boolean(users.length) && <User /> }
→ { users.length ? <User /> : null }
Я обычно использую первый подход при проверке users.length > 0
. Однако вы можете применять любой из вышеперечисленных способов.
Оператор !! преобразует любое значение в булево true
или false
.
Поэтому приведенные выше 2-е и 3-е условия эквивалентны.
Тернарный оператор ?
в четвертом случае будет проверять истинность или ложность значения левой части ?, а так как 0
является ложным значением, то будет возвращено null
.
React не будет отображать в UI значения null
, undefined
и boolean
, если они используются в JSX-выражениях, как в этом случае:
{ null }
{ undefined }
{ true }
{ false }
Чтобы булевы значения отображались на экране, нужно преобразовать их в строку, как показано ниже:
const isLoggedIn = true;
{ "" + isLoggedIn }
OR
{ `${isLoggedIn}` }
OR
{ isLoggedIn.toString() }
Читайте также:
- Создание компонента Timeline с React
- React-приложение с шаблонами «Репозиторий» и «Адаптер»
- Как работает React Fiber Reconciler?
Читайте нас в Telegram, VK и Дзен
Перевод статьи Yogesh Chavan: Top 25 React Tips Every Developer Should Know — Part 1