Получение списка персонажей через API

Проверяйте HTTP-статус до res.json(): 404 означает пустой результат поиска. Храните имя и страницу в состоянии, отменяйте старый запрос при их изменении.

Короткий ответ

Что ответить на собеседовании

Проверяйте HTTP-статус до res.json(): 404 означает пустой результат поиска. Храните имя и страницу в состоянии, отменяйте старый запрос при их изменении.

Подробный разбор

Ответ с пояснениями

Условие

Создайте компонент, который будет загружать данные о персонажах из открытого API Rick and Morty.

Формат ответа API

{
    "info": {
        "count": number,
        "pages": number,
        "next": string | null, // ссылка на следующую страницу
        "prev": string | null // ссылка на предыдущую страницу
    },
    "results": [
        { "name": string, "id": number }
    ]
}

Если запрос выполнен с именем, которого нет в базе, сервер возвращает ошибку 404.

Пример функции‑запроса (можно менять имена переменных, но структура кода должна оставаться той же):

function fetchCharacters(query, pageNumber = 1, options = {}) {
    return fetch(
        `https://rickandmortyapi.com/api/character?name=${query}&page=${pageNumber}`,
        options
    )
    .then(res => res.json())
}

export default function App() {
    return '20 minutes adventure'
}

Файл‑пример (json‑ответ от API) приведён ниже и может использоваться для локального тестирования.

{"info":{"count":107,"pages":6,"next":"https://rickandmortyapi.com/api/character?page=2&name=rick","prev":null},"results":[{"id":1,"name":"Rick Sanchez","status":"Alive","species":"Human","type":"","gender":"Male","origin":{"name":"Earth (C-137)","url":"https://rickandmortyapi.com/api/location/1"},"location":{"name":"Citadel of Ricks","url":"https://rickandmortyapi.com/api/location/3"},"image":"https://rickandmortyapi.com/api/character/avatar/1.jpeg","episode":["https://rickandmortyapi.com/api/episode/1","https://rickandmortyapi.com/api/episode/2","https://rickandmortyapi.com/api/episode/3","https://rickandmortyapi.com/api/episode/4","https://rickandmortyapi.com/api/episode/5","https://rickandmortyapi.com/api/episode/6","https://rickandmortyapi.com/api/episode/7","https://rickandmortyapi.com/api/episode/8","https://rickandmortyapi.com/api/episode/9","https://rickandmortyapi.com/api/episode/10","https://rickandmortyapi.com/api/episode/11","https://rickandmortyapi.com/api/episode/12","https://rickandmortyapi.com/api/episode/13","https://rickandmortyapi.com/api/episode/14","https://rickandmortyapi.com/api/episode/15","https://rickandmortyapi.com/api/episode/16","https://rickandmortyapi.com/api/episode/17","https://rickandmortyapi.com/api/episode/18","https://rickandmortyapi.com/api/episode/19","https://rickandmortyapi.com/api/episode/20","https://rickandmortyapi.com/api/episode/21","https://rickandmortyapi.com/api/episode/22","https://rickandmortyapi.com/api/episode/23","https://rickandmortyapi.com/api/episode/24","https://rickandmortyapi.com/api/episode/25","https://rickandmortyapi.com/api/episode/26","https://rickandmortyapi.com/api/episode/27","https://rickandmortyapi.com/api/episode/28","https://rickandmortyapi.com/api/episode/29","https://rickandmortyapi.com/api/episode/30","https://rickandmortyapi.com/api/episode/31","https://rickandmortyapi.com/api/episode/32","https://rickandmortyapi.com/api/episode/33","https://rickandmortyapi.com/api/episode/34","https://rickandmortyapi.com/api/episode/35","https://rickandmortyapi.com/api/episode/36","https://rickandmortyapi.com/api/episode/37","https://rickandmortyapi.com/api/episode/38","https://rickandmortyapi.com/api/episode/39","https://rickandmortyapi.com/api/episode/40","https://rickandmortyapi.com/api/episode/41","https://rickandmortyapi.com/api/episode/42","https://rickandmortyapi.com/api/episode/43","https://rickandmortyapi.com/api/episode/44","https://rickandmortyapi.com/api/episode/45","https://rickandmortyapi.com/api/episode/46","https://rickandmortyapi.com/api/episode/47","https://rickandmortyapi.com/api/episode/48","https://rickandmortyapi.com/api/episode/49","https://rickandmortyapi.com/api/episode/50","https://rickandmortyapi.com/api/episode/51"],"url":"https://rickandmortyapi.com/api/character/1","created":"2017-11-04T18:48:46.250Z"},...]}"```

Ответ

Проверяйте HTTP-статус до res.json(): 404 означает пустой результат поиска. Храните имя и страницу в состоянии, отменяйте старый запрос при их изменении.

Сохраните структуру fetch → then, добавив кодирование имени и проверку статуса:

function fetchCharacters(query, pageNumber = 1, options = {}) {
  return fetch(`https://rickandmortyapi.com/api/character?name=${encodeURIComponent(query)}&page=${pageNumber}`, options)
    .then(res => {
      if (res.status === 404) return { info: { pages: 0 }, results: [] };
      if (!res.ok) throw new Error('Ошибка загрузки');
      return res.json();
    });
}

Компонент с поиском и пагинацией:

import { useEffect, useState } from 'react';
export default function App() {
  const [filter, setFilter] = useState({ query: '', page: 1 });
  const [data, setData] = useState({ info: { pages: 0 }, results: [] });
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState('');
  useEffect(() => {
    const controller = new AbortController();
    setLoading(true); setError('');
    fetchCharacters(filter.query, filter.page, { signal: controller.signal })
      .then(result => { if (!controller.signal.aborted) setData(result); })
      .catch(e => { if (!controller.signal.aborted) setError(e.message); })
      .finally(() => { if (!controller.signal.aborted) setLoading(false); });
    return () => controller.abort();
  }, [filter]);
  return <section>
    <label>Имя <input value={filter.query} onChange={e =>
      setFilter({ query: e.target.value, page: 1 })} /></label>
    {loading ? <p>Загрузка…</p> : error ? <p role="alert">{error}</p> :
      data.results.length ? <ul>{data.results.map(item => <li key={item.id}>{item.name}</li>)}</ul>
      : <p>Ничего не найдено</p>}
    <button disabled={loading || filter.page === 1} onClick={() =>
      setFilter(f => ({ ...f, page: f.page - 1 }))}>Назад</button>
    <span> {filter.page} </span>
    <button disabled={loading || filter.page >= data.info.pages} onClick={() =>
      setFilter(f => ({ ...f, page: f.page + 1 }))}>Вперёд</button>
  </section>;
}

Приведённый в условии псевдо-JSON с типами/многоточием нельзя передавать JSON.parse без преобразования в настоящий fixture.

ИИ-помощник для собеседований

Хочешь уверенно проходить собеседования?

Попробуй ИИ-помощник для собеседований: слышит вас и собеседника, анализирует экран, подсказывает ответы в реальном времени, работает без VPN и не попадает в захват экрана.

Подробнее