9 вопросов на собеседовании, которые должен знать Senior React разработчик

год назад·1 мин. на чтение

Как разработчик на React, вы в конечном итоге испытаете желание сделать следующий большой шаг к роли Senior.

Некоторые разработчики застряли в роли junior или middle разработчика, даже когда уже набрали опыта. Конечно, опыт приходит со временем, но некоторые разработчики могут иметь мышление senior разработчика, но не изучать необходимые темы. Эта статья не будет содержать туториалы, но будет содержать общий обзор ответов, которые вы должны дать. Вот несколько очень распространенных вопросов на собеседовании, которые вам могут задать во время собеседования на React разработчика.

1. Можете ли вы описать ситуацию, когда вам пришлось оптимизировать React приложение, чтобы повысить его производительность? Как вы это сделали?

Как senior React разработчик, вы должны время от времени оптимизировать React приложение для повышения производительности. Понимание жизненного цикла и хуков React может помочь в этом. Некоторые способы оптимизации производительности приложения React могут включать:
  • Избегать ненужные повторные рендеры
  • Использование уникального идентификатора при отображении списков
  • Использование хуков, таких как useMemo и useCallback, для мемоизации функций
  • Ленивая загрузка

2. Как вы управляете состоянием в большом React приложении?

В React есть два типа состояний: локальное и глобальное состояние. Локальное состояние является эксклюзивным для области React компонента. Доступ к глобальному состоянию может получить любой из ваших компонентов. Некоторые распространенные библиотеки для управления состоянием в большом React приложении включают в себя
  • Redux
  • Recoil
  • Jotai
  • Rematch
  • и т.д.

Когда использовать состояние?

Представьте, что вы создаете приложение со списком дел в React. Вы хотите отслеживать список элементов, введенных пользователем, а также логическое значение, указывающее, загружает ли приложение данные из API в данный момент. Например, у вас может быть действие ADD_TODO, которое добавляет новый элемент списка дел в массив, и редьюсер, который соответствующим образом обновляет состояние. У вас также может быть действие SET_LOADING, которое обновляет состояние загрузки. Другим примером может быть корзина покупок, которая отслеживает товары в корзине, даже когда пользователь обновляет или покидает страницу. Если данные передаются только из глобальной переменной в компоненты приложения, можно также применить хук useContext. Это хорошо подходит для работы с темами пользовательского интерфейса приложения и с auth провайдерами.

3. Можете ли вы описать опыт работы со сложной структурой данных в React приложении? Как вы с этим справились?

Для работы со сложными структурами данных в приложении React может потребоваться использовать такие методы, как сопоставление вложенных данных, использование рекурсивных компонентов для рендеринга данных с несколькими уровнями вложенности и оптимизация производительности с помощью таких методов, как React.memo. Также может быть полезно использовать библиотеки, такие как lodash, для манипулирования и преобразования сложных структур данных. Очевидно, что существует множество способов обработки сложных структур данных в React. Ниже приведено несколько сценариев, в которых вам, возможно, придется более осторожно обращаться с обработкой и представлением данных.
  • Вложенные структуры данных, такие как дерево или граф
  • Большие наборы данных, которые необходимо отображать и обрабатывать в виде таблицы или списка
  • Структуры данных с несколькими уровнями вложенности, такие как объект JSON с несколькими слоями вложенных объектов и массивов
  • Постоянно меняющиеся структуры данных, такие как данные в реальном времени из прямой трансляции или подключения к веб-сокету

4. Как вы подходите к тестированию приложения React?

Важно иметь хорошую стратегию тестирования, чтобы убедиться, что ваше приложение React стабильно и работает правильно. Это может включать в себя комбинацию модульных тестов, интеграционных тестов и e2e тестов, а также таких методов, как снэпшот тестирование и TDD (разработка на основе тестирования).
  • Используйте встроенные утилиты тестирования React, такие как React Testing Library и Enzyme, для тестирования рендеринга и поведения компонентов
  • Напишите модульные тесты для отдельных компонентов React, чтобы убедиться, что они правильно работают в изоляции
  • Напишите интеграционные тесты, чтобы проверить взаимодействие между несколькими компонентами и убедиться, что они правильно работают вместе
  • Используйте платформу тестирования, такую как Jest, для запуска и организации тестов
  • Используйте снэпшот тестирование, чтобы убедиться, что отрисовка компонента не изменится неожиданно
  • Используйте разработку на основе тестирования (TDD) для написания тестов перед написанием кода реализации функции.
  • Используйте библиотеку для мокирования, такую как Sinon.js для имитации зависимостей в тестах
  • Напишите e2e тесты для тестирования приложения в целом, имитируя взаимодействие с пользователем в реальном браузере

5. Как вы обрабатываете асинхронные действия в приложении React?

Одним из способов является использование ключевых слов async и await, которые позволяют писать асинхронный код в синхронном стиле. Ниже приведен пример компонента, который выполняет асинхронный вызов API с помощью async и await:
import React, { useState, useEffect } from 'react';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch('https://..../endpoint');
      const data = await response.json();
      setData(data);
    }
    fetchData();
  }, []);

  return (
    <div>
      {data ? (
        <div>{data.message}</div>
      ) : (
        <div>Loading...</div>
      )}
    </div>
  );
}
Еще один способ обработки асинхронных функций в React — использовать библиотеку, такую как axios или fetch, для выполнения вызовов API. Вот пример использования axios:
import React, { useState, useEffect } from 'react';
import axios from 'axios';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    async function fetchData() {
      const response = await axios.get('https://my-api.com/endpoint');
      setData(response.data);
    }
    fetchData();
  }, []);

  return (
    <div>
      {data ? (
        <div>{data.message}</div>
      ) : (
        <div>Loading...</div>
      )}
    </div>
  );
}

6. Можете ли вы рассказать разницу между презентационным и контейнерным компонентом в React?

В React презентационные компоненты связаны с тем, как все выглядит, в то время как компоненты-контейнеры связаны с тем, как все работает. Презентационные компоненты обычно отвечают за рендеринг элементов пользовательского интерфейса на экране. Они получают данные и обратные вызовы в качестве пропсов. Обычно они сосредоточены на рендеринге JSX и не знают о состоянии или действиях приложения. Ниже приведен пример презентационного компонента:
import React from 'react';

function Button(props) {
  return <button>{props.label}</button>;
}
Компоненты-контейнеры обычно отвечают за управление состоянием и действиями. Они содержат логику для получения данных, обработки пользовательского ввода и выполнения других задач. Они знают о состоянии и действиях приложения, а также передают данные и обратные вызовы презентационным компонентам в качестве пропсов. Ниже приведен пример компонента контейнера:
import React, { Component } from 'react';
import Button from './Button';

class Form extends Component {
  state = {
    name: '',
  };

  handleChange = (event) => {
    this.setState({ name: event.target.value });
  };

  handleSubmit = (event) => {
    event.preventDefault();
    // отправка формы
  };

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.name} onChange={this.handleChange} />
        </label>
        <Button label="Submit" />
      </form>
    );
  }
}
Разделение презентационных и контейнерных компонентов может помочь упростить понимание, тестирование и обслуживание кода, отделив проблемы внешнего вида от того, как все работает.

7. Можете ли вы описать, как бы вы реализовали функциональность пагинации в приложении React?

Вот один из способов реализовать пагинацию в React приложении:
  • Определите общее количество страниц, которые вам нужны, исходя из объема имеющихся у вас данных и количества элементов, которые вы хотите отобразить на странице.
  • Добавьте переменную состояния page в компонент для отслеживания текущей страницы.
  • Напишите функцию, которая извлекает данные для определенной страницы и обновляет состояние компонента новыми данными.
  • Визуализируйте пользовательский интерфейс нумерации страниц, который может включать кнопки для перехода к следующей и предыдущей страницам, а также раскрывающийся список для выбора определенной страницы.
  • Добавьте обработчики событий для элементов пользовательского интерфейса пагинации, которые вызывают функцию fetch с соответствующим номером страницы.
Вы также можете даже использовать библиотеки пользовательского интерфейса, такие как Material UI, чтобы ускорить разработку, которая дает вам базовые компоненты для создания функций с разбиением на страницы.

8. Как вы обрабатываете роутинг на стороне клиента в приложении React?

Существует несколько способов обработки роутинга на стороне клиента в приложении React. Одним из популярных способов является использование библиотеки react-router-dom, которая предоставляет компонент <Router> для обработки роутинга и набор компонентов <Route> для определения роутов в приложении. Ниже приведен пример того, как можно использовать react-router-dom для обработки роутинга на стороне клиента в приложении React: Установите библиотеку react-router-dom.
npm install react-router-dom
Импортируйте компоненты <Router> и <Route> из react-router-dom.
import { BrowserRouter as Router, Route } from 'react-router-dom';
Оберните приложение компонентом <Router>.
<Router>
  <App />
</Router>
Определите свои маршруты с помощью компонента <Route>. Компонент <Route> принимает проп path, указывающий путь для роута, и проп component, указывающий компонент для визуализации при сопоставлении роута.
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/users/:id" component={User} />
Компонент <Route> отобразит указанный компонент, когда текущий URL-адрес совпадет с путем роута. Вы можете использовать проп exact, чтобы указать, что маршрут должен быть сопоставлен только в том случае, если путь точно совпадает с текущим URL-адресом. Вы также можете использовать компонент <Link> из react-router-dom для создания ссылок, которые перемещаются между маршрутами в вашем приложении.
import { Link } from 'react-router-dom';
...
<Link to="/about">About</Link>
Подробнее о React Router читайте в серии статей:
  1. Основы React Router
  2. Продвинутые определения маршрутов
  3. Управление навигацией
  4. Подробно о роутерах

9. Можете ли вы рассказать о преимуществах использования React Context API?

React Context API — отличная альтернатива передаче данных без использования пропсов родительского компонента. Это может быть особенно полезно в тех случаях, когда у вас есть глубоко вложенная структура компонентов или если вы хотите передать данные компоненту, который находится на много уровней вниз по дереву. Некоторые преимущества использования React Context API включают в себя:

Устраняет prop drilling

С помощью Context API вы можете избежать необходимости передавать пропсы через несколько уровней компонентов, что может стать утомительным и затруднить чтение и поддержку кода.

Упрощает совместное использование состояния между компонентами

Если у вас есть состояние, которое необходимо совместно использовать между несколькими компонентами, API контекста может упростить это без необходимости поднимать состояние до общего предка.

Повышает производительность

Так как Context API не полагается на React Virtual DOM для передачи данных между компонентами, он может быть более эффективным, чем использование пропсов. Это может быть особенно полезно в тех случаях, когда вы передаете большие объемы данных или часто выполняете повторный рендеринг.

Увеличивает повторное использование кода

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

Что такое Promise в JavaScript

год назад·6 мин. на чтение

Обещания (Promises) — это мощная возможность JavaScript, которая упрощает понимание асинхронных операций и улучшает читаемость кода.

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

Что такое Promises в JavaScript?

Успех (или сбой) асинхронной операции и значение, которое она генерирует, представлены объектом, называемым обещанием. Вместо вложения обратных вызовов он позволяет нам обрабатывать асинхронный код более элегантным и организованным способом с помощью цепочек методов. Обещания могут находиться в одном из трех состояний: pending (ожидание), fulfilled (выполнен) или rejected (отклонен). Pending - начальное состояние Promise, которое означает, что асинхронная операция все еще выполняется. Если операция выполнена успешно, Promise переходит в состояние “выполнен”, а при возникновении проблемы — в состояние “отклонен”.

Этапы жизненного цикла Promise

Давайте подробно рассмотрим каждый этап жизненного цикла Promise на примерах кода:

Ожидание

Объект Promise находится в состоянии ожидания на момент создания. На данный момент асинхронная операция все еще продолжается, и обещание не принимается и не отклоняется. Вот пример:
const promise = new Promise((resolve, reject) => {
  // Асинхронная операция, например, запрос данных через API,
  // resolve(result) или reject(error) будут вызваны позднее
});

Выполнено (Fulfilled)

Обещание переходит в состояние “выполнено”, как только асинхронная операция успешно завершена. На этом этапе связанное значение (результат) становится доступным. Для обработки выполненного обещания мы используем метод .then() Вот пример:
const promise = new Promise((resolve, reject) => {
  // Симуляция асинхронной операции
  setTimeout(() => {
    resolve("Operation succeeded!");
  }, 2000);
});

promise.then((result) => {
  console.log(result); // Вывод: "Operation succeeded!"
});

Отклонено (Rejected)

В случае, если возникает проблема с асинхронной операцией, Promise переходит в отклоненное состояние. Оно обозначает, что операция не удалась, и предоставляет объекту ошибки соответствующую информацию. Для обработки отклоненного Promise мы используем метод .catch() Вот пример:
const promise = new Promise((resolve, reject) => {
  // Симуляция асинхронной операции
  setTimeout(() => {
    reject(new Error("Something went wrong!"));
  }, 2000);
});

promise.catch((error) => {
  console.log(error.message); // Вывод: "Something went wrong!"
});

Цепочка обещаний

Обещания имеют ряд важных преимуществ, в том числе возможность объединять несколько асинхронных операций, что улучшает читаемость кода. Мы достигаем этого, используя метод .then() для возврата нового Promise. Вот пример:
const getUser = () => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронной операции
    setTimeout(() => {
      resolve({ id: 1, name: "John" });
    }, 2000);
  });
};

const getUserPosts = (user) => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронной операции
    setTimeout(() => {
      resolve(["Post 1", "Post 2"]);
    }, 2000);
 });
};

getUser()
  .then((user) => getUserPosts(user))
  .then((posts) => console.log(posts)); // Вывод: ["Post 1", "Post 2"]

Вспомогательные функции для объектов Promise

Наряду с основными методами, предоставляемыми Promise, JavaScript также предоставляет ряд вспомогательных функций, которые улучшают функциональность и удобочитаемость асинхронного кода. Общие задачи упрощаются благодаря этим вспомогательным функциям, которые также улучшают поток управления и обработку ошибок.

Promise.all()

Когда все объекты Promise во входном массиве выполнены, эта функция возвращает новый объект Promise. Вот пример:
const fetchUser = () => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронного вызова API
    setTimeout(() => {
      const user = { id: 1, name: "John" };
      resolve(user);
    }, 2000);
  });
};

const fetchPosts = () => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронного вызова API
    setTimeout(() => {
      const posts = ["Post 1", "Post 2"];
      resolve(posts);
    }, 1500);
  });
};

const fetchComments = () => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронного вызова API
    setTimeout(() => {
      const comments = ["Comment 1", "Comment 2"];
      resolve(comments);
    }, 1000);
  });
};

Promise.all([fetchUser(), fetchPosts(), fetchComments()])
  .then(([user, posts, comments]) => {
    console.log("User:", user);
    console.log("Posts:", posts);
    console.log("Comments:", comments);
  })
  .catch((error) => {
    console.log("Error:", error);
  });
Три функции fetchUser(), fetchPosts() и fetchComments() включены в приведенный выше пример. Для пользовательских данных, пользовательских сообщений и комментариев пользователей каждая функция имитирует асинхронный вызов API, возвращая Promise. Передавая массив Promise ([fetchUser(), fetchPosts(), fetchComments()]) в Promise.all(), мы создаем новый Promise, который выполняется после успешного выполнения каждого Promise в массиве. При обработке выполнения метод .then() применяет синтаксис, деструктурирующий массив, для получения разрешенных значений каждого объекта Promise. Когда в этой ситуации успешно выполняются все обещания, деструктурирование массива присваивает значения fetchUser(), fetchPosts() и fetchComments() переменным user, posts и comments соответственно. Пользователь, публикации и комментарии выводятся в консоль. Если какой-либо из Promises не удался вызывается .catch() и ошибка выводится в консоль. Promise.all() позволяет нам эффективно извлекать несколько асинхронных ресурсов и обрабатывать их все одновременно после успешного завершения каждого запроса.

Promise.race()

Как только какой-либо из объектов Promise во входном массиве выполнится, эта функция возвращает новый объект Promise, который либо выполняется, либо отклоняется. Вот пример:
const fetchResource = (resource, delay) => {
  return new Promise((resolve, reject) => {
    // Симуляция асинхронного вызова API
    setTimeout(() => {
      resolve(`${resource} is fetched successfully in ${delay}ms`);
    }, delay);
  });
};

const resource1 = fetchResource("Resource 1", 2000);
const resource2 = fetchResource("Resource 2", 1500);
const resource3 = fetchResource("Resource 3", 1000);

Promise.race([resource1, resource2, resource3])
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log(error);
  });
В приведенном выше примере есть три функции, называемые fetchResource() которые имитируют асинхронные вызовы API, возвращая Promises. Каждый вызов имитирует время, необходимое для получения определенного ресурса, с разным временем задержки. Когда массив Promises ([resource1, resource2, resource3]) передается методу Promise.race(), создается новый Promise, который выполняется (выполняется успешно или отклоняется) в ответ на любое Promise в массиве Promise.race(). Значение успешного Promise передается в качестве параметра result и выводится в консоли в методе .then(), который используется для обработки выполнения. В этом случае победителем будет считаться тот ресурс, который разрешится первым (т.е. тот, у которого наименьшая задержка), а его значение будет выведено на консоль. Если какой-либо из Promises не удался, то вызывается .catch() и ошибка выводится в консоли. Мы можем выполнить несколько асинхронных операций одновременно и отреагировать на результат самой быстрой с помощью метода Promise.race(). Это полезно в ситуациях, когда мы хотим действовать в соответствии с первоначальным ответом или завершением.

Promise.resolve() и Promise.reject()

Без необходимости дополнительных асинхронных операций эти функции позволяют создавать Promise, которые уже были выполнены или отклонены соответственно. Благодаря мощным возможностям, которые эти вспомогательные функции предлагают для манипулирования и управления Promises, асинхронное программирование JavaScript теперь более адаптируемо и выразительно. Promise.resolve() и Promise.reject() используются в следующем примере кода:
const fetchData = (shouldSucceed) => {
  if (shouldSucceed) {
    return Promise.resolve("Data fetched successfully");
  } else {
    return Promise.reject(new Error("Failed to fetch data"));
  }
};

fetchData(true)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log(error);
  });

fetchData(false)
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.log(error);
  });
Функция fetchData() в приведенном выше примере имеет параметр shouldSucceed, который указывает, должна ли выборка данных быть успешной или неудачной. Promise.resolve() используется для создания и возврата объекта Promise, который немедленно выполняется с сообщением «Data fetched successfully», если shouldSucceed имеет значение true. Promise.reject() используется для создания и возврата объекта Promise, который немедленно отклоняется с новым объектом Error и сообщением «Failed to fetch data», если shouldSucceed имеет значение false. Возвращенное обещание выполняется в первом вызове fetchData() с shouldSucceed, равным true, и выполнение управляется методом .then(). В result передается значение «Data fetched successfully», которое затем выводится в консоль. Во втором вызове fetchData() с shouldSucceed, равным false, возвращенный объект Promise отклоняется, и для обработки отклонения используется .catch(). Объект ошибки, содержащий сообщение «Failed to fetch data», передается в качестве параметра error и выводится в консоль. Используя Promise.resolve() и Promise.reject() мы можем легко создавать Promise, которые уже разрешены или отклонены, без необходимости дополнительных асинхронных операций. Это полезно при обработке синхронных значений или ошибок в виде объектов Promise.

Итоги

Асинхронное программирование JavaScript произвело революцию благодаря обещаниям, которые предлагают хорошо организованный и красивый способ решения трудоемких задач. Обещания помогают нам создать более удобочитаемый и поддерживаемый код. Определение объектов Promise, этапы их жизненного цикла и вспомогательные функции, таких как Promise.all(), Promise.race(), Promise.resolve() и Promise.reject() были рассмотрены в этом обширном руководстве. Мы можем эффективно обрабатывать асинхронные операции и изящно обрабатывать сценарии успеха и ошибок, понимая этапы жизненного цикла - pending, fulfilled и rejected. Разработчики могут создавать надежные и эффективные приложения, которые легко справляются со сложными асинхронными задачами, используя Promises и их вспомогательные функции. Поток управления оптимизирован, а удобочитаемость кода улучшена за счет цепочек Promise. Кроме того, вспомогательные функции JavaScript улучшают обработку ошибок и упрощают выполнение многочисленных асинхронных операций. Современная веб-разработка требует использования асинхронного программирования, а знание обещаний позволяет разработчикам создавать код, который является более чистым и простым в обслуживании. Вы можете создавать надежные приложения, которые эффективно обрабатывают асинхронные операции, интегрируя Promises в свои JavaScript проекты и используя их вспомогательные функции.