В интернете очень мало качественных примеров лучших практик написания Реакт + ТС кода, особенно через ревью, все они банальные и описывают простейшие принципы, я постараюсь на маленьком примере капнуть чуть глубже, хотя конечно эта тема необьятная и если сильно зайдет, оставляйте фидбек мне в телеграмм, сделаю вторую часть)
Рассмотрим пример
Скажем у меня есть код двух реакт компонентов
Компонент Header.tsx
import Avatar from "@components/Avatar";
import { useEffect, useState } from 'react';
import UserAPI from "@api/UserAPI";
export default function Header() {
const [user, setUser] = useState(null);
useEffect(() => {
UserAPI.fetchUser().then((userData) => {
setUser(userData);
});
});
return (
<div className="Header">
<Avatar user={user} />
</div>
);
}Компонент Avatar.tsx
import React, {FC, useMemo, ImgHTMLAttributes} from 'react';
import "./avatar.scss";
const avatarTmpl = require('@root/images/no-avatar.png').default;
type Props = {
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
} & ImgHTMLAttributes<HTMLImageElement>;
const Avatar: FC<Props> = ({
src = avatarTmpl,
onInputChange = undefined,
user,
...rest
}) => {
const { firstName, lastName } = user;
const fullName = useMemo(() => `${firstName} ${lastName}`);
return (
<div className="profile-avatar">
<input
type="file"
className="profile-avatar__input"
onChange={(event) => {
onInputChange(event);
}}
/>
<span>{fullName}</span>
<img
className="profile-avatar__image"
src={src}
alt={rest.alt}
/>
</div>
)
};
export default Avatar;На их примере я постараюсь разобрать как делать не надо и как надо :)
Простейшие первоначальные принципы
В первом же компоненте Header.tsx мы видим банальные ошибки
- Конечно же useEffect нужно добавить массив зависимостей. Если массива зависимостей нет, то эффект будет отрабатывать на каждое изменение компонента, соответственно большинство раз будут лишними, а в данном случае вообще вызовет бесконечный цикл
- Типизация, в тайпскрипте важно соблюдать тонкую грань, типизизровать явно то, что может либо меняться по ходу выполнения кода, либо те вещи, тип которых не очевиден с первого взгляда
Пример
// Излишняя типизация, очевидно что 5 - число и тк const, меняться оно не будет)
const num: number = 5;
// А вот тут лучше передать тип дженериком, иначе получается неочевидно
const [user, setUser] = useState<UserType | null>(null);- Семантика имеет значение. Если вы пишете проект на фреймворке - это не значит что вы должны подвергаться диватозу и делать весь сайт на дивах, как минимум нужно одергивать себя, что если у элемента класс 'header, видимо, этот элемент header
- в аватар передается пропс user, и здесь есть два важных момента, первый - я бы передавал firstName={user.firstName} и lastName={user.lastName}, это хорошо по двум причинам, первая - потому что если обьект юзера в дальнейшем разрастется, мы не будем передавать ничего лишнего в пропсы компонента, это поможет избежать некоторых перерисовок, и второе - будет очевидно что именно нужно данному компоненту, читаться и дебажиться код будет легче, второй ключевой момент - user может быть null, я бы сделал user ?? {} или что-то похожее
Грамотная типизация компонентов + dummy ui typings
В компоненте Avatar.tsx мы видим вот такую типизацию
type Props = {
onInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
} & ImgHTMLAttributes<HTMLImageElement>;& - синтаксис Union типов, то есть тип Props будет обязательно включать все типы HTML элемента img (например src и alt), а так же onInputChange функцию В каких случаях нам это нужно?
В случаях когда мы пишем собственную ui библиотеку
Пример компонента
import {DetailedHTMLProps, HTMLAttributes, ReactNode} from 'react';
import styles from './Htag.module.css'
interface HtagProps extends DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>{
tag: 'h1' | 'h2' | 'h3';
children: ReactNode;
}
export const Htag = ({ tag, children, ...props }: HtagProps): JSX.Element => {
switch (tag) {
case "h1":
return <h1 className={styles.h1} {...props}>{children}</h1>
case "h2":
return <h2 className={styles.h2} {...props}>{children}</h2>
case "h3":
return <h3 className={styles.h3} {...props}>{children}</h3>
default:
return <></>
}
};Что происходит в примере ниже? По сути то же самое что было в типах компонента Avatar, мы берем Htag пропс и наследуем их от пропсов HTML элемента H, заголовка.
Именно для этого мы в return спредим пропсы в элемент, для чего это делается?
У h элемента не так много специфических пропсов, но у того же img есть src и alt например, и делается это для того чтобы мы могли в компонент Image нашего ui-kit который будет оберткой над img html элементов передавать эти пропсы без проблем
Для этого мы расширяем тип пропсов и спредим пропсы, в случае компонента Avatar - это оверхед, это просто не нужно.
Но в случае Avatar компонента - туда принимается минимум три пропса, а типизируется один, вот в этом как раз ошибка.
Пропсы src и user в идеале тоже типизировать, а так как src в ImgHTMLAttributes - его можно не типизировать, а вот user обязательно
onInputChange должен быть типизирован так
onInputChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;С вопросительным знаком чтобы элемент мог быть undefined, а когда он типизирован как функция, то при текущей запиши в пропсах компонента
onInputChange = undefinedБудет тс ошибка
Далее в компоненте деструктурируется firstName, lastName из обьекта user - который может быть null, это небезопасно и на этом моменте компонент тоже может упасть
useMemo
Даже опытные разработчики допускают ошибки с этим хуком.
Как он работает под капотом?
- Создает обьект кеша
- При первом запуске вычисляет значение функции и кладет в обьект кеша
- При последующих запусках проверяет аргументы
- Если аргументы изменились - вычисляет функцию заново
- Если аргументы остались прежними - достает значение из кеша
Этот хук не рекомендуется использовать просто так, потому что создавать обьект и хранить его постоянно в памяти, так же сохраняя предыдущие аргументы функции и каждый раз сравнивать, плюс обращаться к обьекту либо выполнять вычисления - дорогостоящая операция.
Если ваше вычисление очень тяжеловесное, то вы действительно сэкономите производительность в том случае когда вычисление не произойдет а значение просто возьмется из кеша, но если вы просто интерполируете строки, как в данном случае - это вычисление ничего не стоит, и обращаться постоянно к обьекту в памяти, который там все это время хранится и занимает место просто не выгодно, проще на каждый рендер заново интерполировать строки
Поэтому не используйте useMemo просто так, знайте его реальную цену.
В некоторых случаях useMemo можно эффективно использовать для вычисления и сохранения данных, вместо useEffect, но это более продвинутый способ, в котором тоже постоянно нужно следить за тем,
- Выгодно ли это
- Не обращаемся ли мы при этом к сторонним (даже браузерным API), потому что все API вызовы должны быть в эффекте
Все последующие проблемы в этом компоненте очевидные либо похожие на те что мы уже разобрали, например небезопасное обращение к обьекту rest.alt, мы не уверены что alt там будет потому что rest не типизирован и альт передается не явно, ну и опять же, div, хотя если внутри инпут - это должен быть form
Спасибо за внимание, пишите чистый код, не допускайте глупых ошибок, так победим!