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

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

Роман Хаимов

Роман Хаимов

, старший инженер-программист практики 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 работают вместе. Это улучшит ваше понимание своей работы и, как следствие, сделает вас более эффективным разработчиком, особенно при решении проблем со сборкой.

 

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