Обложка статьи «Типичные ошибки джунов, использующих React»

Типичные ошибки джунов, использующих React

Перевод статьи «Mistakes Junior React Developers Make»

Роман Хаимов

Роман Хаимов, , старший инженер-программист практики Frontend компании «Рексофт»

В материале приведены некоторые из ошибок, которые React-разработчики совершают на раннем этапе своего профессионального роста.

Прямые манипуляции с DOM

Такого рода ошибка встречается особенно часто среди разработчиков, которые только пересели с jQuery.

Писали ли вы такой код?

import React from "react";

export class Menu extends React.Component {
  componentDidMount() {
    if (window.innerWidth < 500) {
      const menu = document.querySelector(".menu");
      menu.classList.add("mobile");
    }
  }
  render() {
    return <div className="menu">{this.props.children}</div>;
  }
}

В чем же проблема?

В React мы должны избегать прямых взаимодействий с DOM. Вместо того чтобы получать ссылку на DOM узел и добавлять к нему класс напрямую, следует изменить состояние в самом компоненте, исходя из которого и будет добавлен класс к элементу.

Что же такого плохого в прямых манипуляциях DOM?

Любое веб-приложение на самом деле строится на состоянии и его управлении. Есть прямая зависимость между сложностью ПО и состоянием, которое его описывает. Если ваше приложение совмещает состояния DOM и React, то сложность его поддержки вырастет очень быстро.

Возможное решение

import React from "react";
export class Test extends React.Component {
  state = {
    isMobile: false
  };
  componentDidMount() {
    if (window.innerWidth < 500) {
      this.setState({
        isMobile: true
      });
    }
  }
  render() {
    return (
      <div className={this.state.isMobile ? "mobile" : ""}>
        {this.props.children}
      </div>
    );
  }
}

Обратим внимание на то, как мы используем React состояние для обновления атрибута className в нашем компоненте, и, как следствие, мы избавились от document.querySelector. Отлично!

Не следить за ресурсами

Новички в разработке часто пишут такой код, когда работают с событиями. Возьмем, например, простой React компонент, который делает… что-то, когда пользователь нажмет пробел.

import React from "react";

export class CaptureSpace extends React.Component {
  componentDidMount() {
    document.body.addEventListener("keydown", event => {
        if (event.keyCode === 32) {
          // do something when user hits spacebar
        }
    });
  }
render() {
    return (
       //
    );
  }
}

Заметили, как мы добавили слушатель события, но не позаботились о том, чтобы удалить его в конце?

Это может привести к утечкам памяти и трудноуловимым проблемам в будущем. Лучшим решением считается удаление подписчиков перед тем, как наш компонент будет удален из DOM.

Взглянем на решение ниже:

import React from "react";

export class CaptureSpace extends React.Component {
  componentDidMount() {
    document.body.addEventListener("keydown", this.captureSpace);
  }
componentWillUnmount() {
    document.body.removeEventListener("keydown", this.captureSpace);
  }
captureSpace = event => {
    if (event.keyCode === 32) {
      // do something when user hits spacebar
    }
  };
render() {
    return (
       //
    );
  }
}

Отказ от тестов (или их недостаточное количество)

Если бы мне давали по рублю за каждый проект, который я просмотрел и где единственным тестом был тот, что является по умолчанию в create-react-app, я не писал бы эту статью. А, наверное, потягивал сейчас дайкири где-нибудь на пляже.

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

Может быть, заглушки сбивают их с толку? Или они испытывают сложности с тем, что следует тестировать и что нет?

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

import React from "react";
import axios from "axios";

export const LoginForm = props => {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
return (
    <form
      onSubmit={async event => {
        event.preventDefault();
        const result = await      axios.post("https://reqres.in/api/login", {
          email,
          password
        });
       if (result.data.token) {
          window.localStorage.setItem("token", result.data.token);
          window.location.replace("/home");
        }
      }}
    >
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          onChange={event => setEmail(event.target.value)}
          value={email}
        />
      </div>
      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          onChange={event => setPassword(event.target.value)}
          value={password}
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  );
};

Так как же протестировать данную форму?

Первым делом давайте взглянем, каким образом наш пользователь будет взаимодействовать с ней.

  1. Пользователь вводит свои данные.
  2. Пользователь нажимает на кнопку подтверждения.
  3. Пользователь перенаправляется на страничку «home».

Это и есть все то, что нам нужно протестировать.

Ниже я написал один из тест-кейсов для данного компонента. Можете ли вы предложить какие-нибудь еще сценарии, которые было бы полезно протестировать?

import React from "react";
import { LoginForm } from "./Login";
import axios from "axios";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

jest.mock("axios");
describe("LoginForm", () => {
  describe("when a user types in a correct email and password", () => {
    it("should redirect to home page", async () => {
      const rendered = render(<LoginForm />);
      const emailInput = rendered.getByLabelText("Email");
      await userEvent.type(emailInput, "john@gmail.com");
      const passwordInput = rendered.getByLabelText("Password");
      await userEvent.type(passwordInput, "1234");
      delete window.location;
      window.location = { replace: jest.fn() };
      const data = { data: { token: "fake-token" } };
      axios.post.mockImplementationOnce(() => Promise.resolve(data));
      userEvent.click(rendered.getByText("Submit"));
      expect(window.location.replace).toHaveBeenCalledWith("/home");
    });
  });
});

Непонимание Webpack

Некоторые из младших разработчиков, с которыми я работал, знали, как использовать, но не понимали, как работает Webpack. Они использовали только лишь с основной кодовой базой проекта и считали, что все остальное «работает просто потому что». Они не копали глубже, не выясняли, как именно CSS и ES6, который они пишут, трансформируется и объединяется в то, что в конечном итоге используется клиентским браузером.

Я рекомендую каждому React-разработчику выделить время и построить простой шаблонный проект. Вместо того, чтобы каждый раз полагаться на create-react-app и NextJS, разберитесь, как современные инструменты сборки JavaScript работают вместе. Это улучшит ваше понимание своей работы и, как следствие, сделает вас более эффективным разработчиком, особенно при решении проблем со сборкой.