Лучшие практики написания React кода

В интернете очень мало качественных примеров лучших практик написания Реакт + ТС кода, особенно через ревью, все они банальные и описывают простейшие принципы, я постараюсь на маленьком примере капнуть чуть глубже, хотя конечно эта тема необьятная и если сильно зайдет, оставляйте фидбек мне в телеграмм, сделаю вторую часть)

Рассмотрим пример

Скажем у меня есть код двух реакт компонентов

Компонент 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 мы видим банальные ошибки

  1. Конечно же useEffect нужно добавить массив зависимостей. Если массива зависимостей нет, то эффект будет отрабатывать на каждое изменение компонента, соответственно большинство раз будут лишними, а в данном случае вообще вызовет бесконечный цикл
  2. Типизация, в тайпскрипте важно соблюдать тонкую грань, типизизровать явно то, что может либо меняться по ходу выполнения кода, либо те вещи, тип которых не очевиден с первого взгляда

Пример

// Излишняя типизация, очевидно что 5 - число и тк const, меняться оно не будет)
const num: number = 5;

// А вот тут лучше передать тип дженериком, иначе получается неочевидно
const [user, setUser] = useState<UserType | null>(null);
  1. Семантика имеет значение. Если вы пишете проект на фреймворке - это не значит что вы должны подвергаться диватозу и делать весь сайт на дивах, как минимум нужно одергивать себя, что если у элемента класс 'header, видимо, этот элемент header
  2. в аватар передается пропс 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

Даже опытные разработчики допускают ошибки с этим хуком.

Как он работает под капотом?

  1. Создает обьект кеша
  2. При первом запуске вычисляет значение функции и кладет в обьект кеша
  3. При последующих запусках проверяет аргументы
  4. Если аргументы изменились - вычисляет функцию заново
  5. Если аргументы остались прежними - достает значение из кеша

Этот хук не рекомендуется использовать просто так, потому что создавать обьект и хранить его постоянно в памяти, так же сохраняя предыдущие аргументы функции и каждый раз сравнивать, плюс обращаться к обьекту либо выполнять вычисления - дорогостоящая операция.

Если ваше вычисление очень тяжеловесное, то вы действительно сэкономите производительность в том случае когда вычисление не произойдет а значение просто возьмется из кеша, но если вы просто интерполируете строки, как в данном случае - это вычисление ничего не стоит, и обращаться постоянно к обьекту в памяти, который там все это время хранится и занимает место просто не выгодно, проще на каждый рендер заново интерполировать строки

Поэтому не используйте useMemo просто так, знайте его реальную цену.

В некоторых случаях useMemo можно эффективно использовать для вычисления и сохранения данных, вместо useEffect, но это более продвинутый способ, в котором тоже постоянно нужно следить за тем,

  1. Выгодно ли это
  2. Не обращаемся ли мы при этом к сторонним (даже браузерным API), потому что все API вызовы должны быть в эффекте

Все последующие проблемы в этом компоненте очевидные либо похожие на те что мы уже разобрали, например небезопасное обращение к обьекту rest.alt, мы не уверены что alt там будет потому что rest не типизирован и альт передается не явно, ну и опять же, div, хотя если внутри инпут - это должен быть form

Спасибо за внимание, пишите чистый код, не допускайте глупых ошибок, так победим!